Eliminowanie przerw w celu zwiększenia wydajności renderowania

Tom Wiltzius
Tom Wiltzius

Wstęp

Chcesz, aby aplikacja internetowa reagowała i reagowała płynnie podczas animacji, przejść i innych drobnych efektów interfejsu. Dbanie o to, żeby te efekty były pozbawione drgań, może oznaczać różnicę między stylem „natywnym” a niezgrabnym, niedopracowanym.

Jest to pierwszy z serii artykułów na temat optymalizacji wydajności renderowania w przeglądarce. Na początek omówimy, dlaczego płynna animacja jest trudna i co jest konieczne, aby ją osiągnąć. Pokażemy też kilka prostych sprawdzonych metod. Wiele z tych pomysłów zostało pierwotnie zaprezentowanych w tym roku „Jank Busters”, który przeprowadził Nat Duca i ja wygłosili oni podczas tegorocznej konferencji Google I/O (film).

Przedstawiamy synchronizację pionową

Gracze komputerowi mogą znać ten termin, ale w internecie jest on rzadko używany. Co to jest synchronizacja V?

Zwróć uwagę na wyświetlacz telefonu: odświeża się on w regularnych odstępach czasu, zwykle (ale nie zawsze!) około 60 razy na sekundę. Synchronizacja pionowa (V) oznacza proces generowania nowych klatek tylko między odświeżeniami ekranu. Jest to coś w stylu wyścigu między zapisem danych do bufora ekranu a odczytem tych danych przez system operacyjny w celu wyświetlenia ich na ekranie. Chcemy, aby zawartość buforowanej klatki zmieniała się pomiędzy odświeżeniami, a nie podczas ich trwania. W przeciwnym razie monitor będzie widzieć połowę jednej klatki, a druga, co doprowadzi do „rozerwania”.

Aby animacja była płynna, przy każdym odświeżeniu ekranu musi być gotowa nowa klatka. Ma to 2 główne konsekwencje: czas renderowania klatki (czyli czas potrzebny na przygotowanie klatki) i budżet ramki (czyli czas potrzebny przeglądarce na utworzenie klatki). Pomiędzy odświeżeniami ekranu masz tylko czas między odświeżeniami ekranu (ok. 16 ms na ekranie 60 Hz) i chcesz zacząć tworzyć kolejną klatkę od razu po umieszczeniu ostatniej na ekranie.

Wszystko zależy od czasu: requestAnimationFrame

Wielu programistów stron internetowych tworzy animacje za pomocą setInterval lub setTimeout co 16 milisekund. Jest to problem z wielu powodów (które omówimy bardziej szczegółowo za chwilę), ale szczególnie istotne są:

  • Rozdzielczość licznika czasu w JavaScript jest liczona tylko na kilka milisekund.
  • Różne urządzenia mają różną częstotliwość odświeżania

Przypomnij sobie omówiony powyżej problem z czasem renderowania klatki: aby przygotować się przed następnym odświeżeniem ekranu, potrzebujesz ukończonej klatki animacji z użyciem JavaScriptu, modyfikacji DOM, układu, malowania itp. Niska rozdzielczość licznika czasu może utrudniać ukończenie klatek animacji przed następnym odświeżeniem ekranu, ale zmienność częstotliwości odświeżania ekranu sprawia, że jest to niemożliwe, gdy ustawiony jest stały licznik czasu. Niezależnie od tego, ile wynosi interwał licznika czasu, będziesz powoli odsuwać się poza ramkę czasową dla danej klatki i w końcu ją tracić. Dzieje się tak nawet wtedy, gdy licznik czasu uruchamiał się z dokładnością do milisekundy, a nie (co zostało wykryte przez programistów) – rozdzielczość licznika zależy od tego, czy komputer jest na baterii czy podłączony, czy może być uzależniony od tego, czy karty w tle powodują przeciążenie zasobów itp. Nawet jeśli zdarza się to rzadko (np. co 16 klatek z powodu braku aktywności o 16 sekund), zauważysz, że komunikat będzie osłabiony przez kilka sekund: Będziesz też generować klatki, które nigdy się nie wyświetlają, co ma wpływ na zużycie energii i czas pracy procesora, który poświęcasz na inne rzeczy w aplikacji.

Różne wyświetlacze mają różne częstotliwości odświeżania: 60 Hz to powszechna częstotliwość, ale w przypadku niektórych telefonów częstotliwość 59 Hz to 59 Hz, w przypadku niektórych laptopów częstotliwość odświeżania spada do 50 Hz, a w przypadku innych monitorów – 70 Hz.

Mówiąc o wydajności renderowania, zwykle skupiamy się na klatkach na sekundę (FPS), ale ta różnica może stanowić jeszcze większy problem. Nasze oczy zwracają uwagę na drobne, nieregularne elementy animacji, które są w stanie wygenerować niewłaściwie ustrojona animacja.

Aby uzyskać prawidłowe klatki animacji o prawidłowym czasie trwania, skorzystaj z requestAnimationFrame. Podczas korzystania z tego interfejsu API prosisz przeglądarkę o ramkę animacji. Wywołanie zwrotne jest wywoływane, gdy przeglądarka wkrótce utworzy nową ramkę. Dzieje się tak bez względu na częstotliwość odświeżania.

requestAnimationFrame ma też inne przydatne właściwości:

  • Animacje na kartach w tle są wstrzymywane, co oszczędza zasoby systemowe i żywotność baterii.
  • Jeśli system nie potrafi obsłużyć renderowania z częstotliwością odświeżania ekranu, może ograniczać animacje i rzadziej wykonywać wywołanie zwrotne (np. 30 razy na sekundę na ekranie 60 Hz). Choć liczba klatek spada o połowę, animacja pozostaje spójna. Jak już wspomnieliśmy, nasze oczy bardziej zwracają uwagę na wariancję niż liczba klatek na sekundę. Stała częstotliwość 30 Hz wygląda lepiej niż 60 Hz, która powoduje pominięcie kilku klatek na sekundę.

Rozwiązanie requestAnimationFrame zostało już omówione w wielu miejscach. Więcej informacji na ten temat znajdziesz w artykułach takich jak ten artykuł dotyczący kreacji JS. To jednak ważny pierwszy krok do zapewnienia płynności animacji.

Budżet ramki

Chcemy, aby przy każdym odświeżeniu ekranu była tworzona nowa ramka, dlatego między odświeżeniami upływa tylko czas między odświeżeniami, który wymaga wykonania wszystkich czynności związanych z utworzeniem nowej klatki. Na wyświetlaczu z częstotliwością 60 Hz czas oczekiwania wynosi ok. 16 ms na uruchomienie JavaScriptu, wykonanie układu, malowanie i wykonanie innych czynności, które przeglądarka musi wykonać, aby wydobyć ramkę. Oznacza to, że jeśli kod JavaScript w wywołaniu zwrotnym requestAnimationFrame trwa dłużej niż 16 ms, nie ma obaw, że uda się wyrenderować klatkę na czas v-sync.

16 ms to niewiele czasu. Na szczęście Narzędzia dla programistów w Chrome pomagają w ustaleniu, czy w trakcie wywołania zwrotnego requestAnimationFrame nadużywasz budżetu klatek.

Otwarcie osi czasu w Narzędziach deweloperskich i nagranie tej animacji w praktyce pokazuje, że podczas tworzenia animacji znacznie przekraczamy budżet. Na osi czasu włącz „Klatki” i sprawdź:

Wersja demonstracyjna ze zbyt dużym układem
Wersja demonstracyjna ze zbyt dużym układem

Wywołania zwrotne requestAnimationFrame (rAF) trwają dłużej niż 200 ms. To rząd wielkości za długi, aby zaznaczać klatkę co 16 ms. Otwarcie jednego z takich długich wywołań zwrotnych rAF pokazało, co się dzieje w środku – w tym przypadku jest wiele układu.

Film Pawła zawiera więcej informacji o konkretnej przyczynie sztafety (treść jest scrollTop) i o tym, jak jej uniknąć. Możesz jednak przeanalizować tę kwestię i sprawdzić, co trwa tak długo.

Zaktualizowana wersja demonstracyjna ze znacznie ograniczonym układem
Zaktualizowana wersja demonstracyjna ze znacznie ograniczonym układem

Zwróć uwagę na 16 ms klatek na sekundę. Ta pusta przestrzeń w ramkach to obszar, w którym musisz wykonać więcej pracy (lub pozwolić przeglądarce robić to, co ma robić w tle). Ta pusta przestrzeń to coś dobrego.

Inne źródło Jank

Największą przyczyną problemów przy uruchamianiu animacji JavaScript jest to, że inne rzeczy mogą przeszkodzić w wywołaniu zwrotnym rAF, a nawet całkowicie uniemożliwić jego uruchomienie. Nawet jeśli wywołanie zwrotne rAF jest mało wydajne i trwa tylko kilka milisekund, inne działania (takie jak przetwarzanie właśnie odebranego XHR, uruchamianie modułów obsługi zdarzeń wejściowych lub przeprowadzanie zaplanowanych aktualizacji na liczniku czasu) mogą nagle pojawić się i uruchomić przez dowolny czas bez skutków. Przetwarzanie tych zdarzeń na urządzeniach mobilnych może czasem trwać setki milisekund, a w tym czasie animacja jest całkowicie wstrzymana. Takie zaczepy nazywamy zacinaniem.

Nie ma sposobu na uniknięcie takich sytuacji, ale istnieje kilka sprawdzonych metod projektowania, które pomogą Ci osiągnąć sukces:

  • Nie wykonuj za dużo przetwarzania w modułach obsługi danych wejściowych. Wykonywanie dużej ilości kodu JS lub zmienianie układu strony np. z użyciem modułu obsługi przewijania to bardzo częsta przyczyna strasznego męczenia.
  • Przekaż jak najwięcej przetwarzania (odczytu: wszystko, których przetwarzanie będzie długo trwać) do wywołania zwrotnego rAF lub instancji roboczych.
  • Jeśli przekażesz zadanie w wywołaniu zwrotnym rAF, spróbuj je podzielić na fragmenty, aby przetwarzać każdą klatkę, lub opóźnić ją do zakończenia ważnej animacji. W ten sposób możesz dalej uruchamiać krótkie wywołania zwrotne rAF i płynnie się animować.

Świetny samouczek o tym, jak przekazywać przetwarzanie do wywołań zwrotnych requestAnimationFrame zamiast do obsługi danych wejściowych, można znaleźć w artykule Paula Lewisa: Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animacja CSS

Czy jest coś fajniejszego niż prosty JS w zdarzeniach i wywołaniach zwrotnych rAF? Brak kodu JS.

Wcześniej mówiliśmy, że nie ma cudownego rozwiązania, które pozwoli uniknąć przerywania wywołań zwrotnych RAF, ale możesz użyć animacji CSS, aby całkowicie ich uniknąć. Szczególnie w Chrome na Androida (i inne przeglądarki pracują nad podobnymi funkcjami) animacje CSS mają tę pożądaną właściwość, którą przeglądarka często może uruchamiać, nawet jeśli działa JavaScript.

W powyższej sekcji jest niejawne stwierdzenie: przeglądarki mogą robić tylko jedną rzecz naraz. Nie jest to prawda, ale dobrze jest mieć pewność, że w danym momencie przeglądarka może uruchamiać kod JS, wykonywać układ lub malować, ale tylko w tym samym czasie. Możesz to sprawdzić w widoku Oś czasu w Narzędziach deweloperskich. Jednym z wyjątków od tej reguły są animacje CSS w Chrome na Androida (i wkrótce Chrome na komputery, ale jeszcze nie).

Użycie animacji CSS w miarę możliwości upraszcza aplikację i zapewnia płynne jej wyświetlanie nawet przy uruchomieniu JavaScriptu.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

Po kliknięciu przycisku JavaScript działa przez 180 ms, powodując zacinanie. Jeśli jednak wprowadzimy tę animację za pomocą animacji CSS, zacięty problem już się nie pojawia.

(pamiętaj, że w chwili tego tekstu animacje CSS działają płynnie tylko w Chrome na Androida, a nie w Chrome na komputery).

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

Więcej informacji o korzystaniu z animacji CSS znajdziesz w tym artykule w MDN.

Podsumowanie

Krótkie podsumowanie:

  1. W przypadku animacji istotne jest tworzenie klatek przy każdym odświeżeniu ekranu. Zsynchronizowane animacje mają ogromny pozytywny wpływ na sposób działania aplikacji.
  2. Najlepszym sposobem na uzyskanie animacji vsync w Chrome i innych nowoczesnych przeglądarkach jest użycie animacji CSS. Jeśli potrzebujesz większej elastyczności niż zapewnia animacja CSS, najlepszą metodą jest animacja oparta na requestAnimationFrame.
  3. Aby utrzymać dobry stan animacji rAF, upewnij się, że inne moduły obsługi zdarzeń nie przeszkadzają w uruchomieniu wywołania zwrotnego rAF i dbaj o to, by wywołania zwrotne rAF były krótkie (poniżej 15 ms).

Animacje w formacie vsync mają zastosowanie nie tylko do prostych animacji w interfejsie – dotyczą one animacji Canvas2D i WebGL, a nawet przewijania na stronach statycznych. W następnym artykule z tej serii omówimy skuteczność przewijania, biorąc pod uwagę te kwestie.

Miłego tworzenia animacji!

Źródła