Eliminowanie przerw w celu zwiększenia wydajności renderowania

Tom Wiltzius
Tom Wiltzius

Wprowadzenie

Aplikacja internetowa powinna działać płynnie i błyskawicznie reagować na animacje, przejścia i inne drobne efekty interfejsu. Uzyskanie efektów bez zacięć może oznaczać różnicę między „natywnym” a niedopracowanym efektem.

To pierwszy z cyklu artykułów dotyczących optymalizacji wydajności renderowania w przeglądarce. Na początek wyjaśnimy, dlaczego płynna animacja jest trudna do wykonania i co trzeba zrobić, aby ją uzyskać. Podpowiemy też, jak stosować kilka prostych, ale sprawdzonych metod. Wiele z tych pomysłów zostało pierwotnie przedstawionych w prezentacji „Jank Busters”, którą przeprowadziliśmy z Nat Duca podczas tegorocznej konferencji Google I/O (wideo).

Przedstawiamy V-sync

Gracze na PC mogą być zaznajomieni z tym terminem, ale w internecie jest on rzadko używany: czym jest synchronizacja pionowa?

Wyobraź sobie wyświetlacz telefonu: odświeża się w regularnych odstępach czasu, zwykle (ale nie zawsze!) około 60 razy na sekundę. Synchronizacja pionowa (czyli pionowa synchronizacja) polega na generowaniu nowych klatek tylko podczas odświeżania ekranu. Można to traktować jako warunek wyścigu między procesem zapisującym dane do bufora ekranu a systemem operacyjnym odczytującym te dane w celu wyświetlenia ich na ekranie. Chcemy, aby zawartość buforowanego obrazu zmieniała się między odświeżeniami, a nie podczas nich. W przeciwnym razie monitor będzie wyświetlać połowę jednej klatki i połowę drugiej, co spowoduje „przerwanie”.

Aby uzyskać płynną animację, musisz mieć gotową nową klatkę za każdym razem, gdy następuje odświeżanie ekranu. Ma to 2 ważne konsekwencje: czas wyświetlania klatki (czyli czas, w którym klatka musi być gotowa) i budżet klatek (czyli czas, jaki przeglądarka ma na wygenerowanie klatki). Na wykonanie klatki masz tylko czas między odświeżeniami ekranu (około 16 ms na ekranie 60 Hz), a chcesz rozpocząć generowanie następnej klatki, gdy tylko ostatnia zostanie wyświetlona.

Czas wykonania ma znaczenie: requestAnimationFrame

Wielu webmasterów używa funkcji setInterval lub setTimeout co 16 milisekund, aby tworzyć animacje. Jest to problem z wielu powodów (o których opowiemy za chwilę), ale szczególnie istotne są:

  • Rozdzielczość timera z JavaScriptu wynosi tylko kilka milisekund.
  • Różne urządzenia mają różne częstotliwości odświeżania.

Przypomnij sobie wspomniany powyżej problem z synchronizacją klatek: przed kolejną aktualizacją ekranu musisz mieć gotową klatkę animacji, która jest już gotowa do użycia, a także musisz zakończyć wszelkie operacje JavaScript, manipulację DOM, układ, malowanie itp. Niska rozdzielczość może utrudniać tworzenie klatek animacji przed kolejnym odświeżeniem ekranu, ale zmienna częstotliwość odświeżania ekranu uniemożliwia to przy stałym liczniku. Niezależnie od tego, jak długi jest interwał licznika czasu, prędzej czy później skończy się czas na wyświetlenie klatki i zostanie ona odrzucona. Wystąpiłoby to nawet wtedy, gdyby zegar działał z dokładnością do milisekund, co nie jest możliwe (jak stwierdzili deweloperzy). Rozdzielczość zegara zależy od tego, czy urządzenie jest podłączone do zasilania, czy działa na baterii, a także może być ograniczona przez zasoby zużywane przez karty działające w tle. Nawet jeśli zdarza się to rzadko (np. co 16 klatek, ponieważ zegar był o milisekundę za wcześnie), zauważysz, że w sekundzie wypada kilka klatek. Będziesz też generować klatki, które nigdy nie zostaną wyświetlone, co powoduje marnowanie mocy i czasu procesora, który można wykorzystać na inne zadania w aplikacji.

Różne wyświetlacze mają różne częstotliwości odświeżania: 60 Hz jest powszechne, ale niektóre telefony mają 59 Hz, niektóre laptopy w trybie niskiego poboru mocy schodzą do 50 Hz, a niektóre monitory do komputerów mają 70 Hz.

Podczas omawiania wydajności renderowania skupiamy się na liczbie klatek na sekundę (FPS), ale zmienność może być jeszcze większym problemem. Nasze oczy dostrzegają drobne, nieregularne zacięcia animacji, które mogą być spowodowane nieprawidłowym ustawieniem czasu.

Aby uzyskać prawidłowo zsynchronizowane klatki animacji, użyj requestAnimationFrame. Gdy używasz tego interfejsu API, prosisz przeglądarkę o ramkę animacji. Funkcja wywołania zwrotnego jest wywoływana, gdy przeglądarka wkrótce wygeneruje nowy kadr. Dzieje się tak niezależnie od częstotliwości odświeżania.

requestAnimationFrame ma też inne zalety:

  • Animacje na kartach w tle są wstrzymywane, co pozwala oszczędzać zasoby systemu i wydłużać czas pracy na baterii.
  • Jeśli system nie może renderować przy częstotliwości odświeżania ekranu, może ograniczyć animacje i wywołania zwrotne (np. 30 razy na sekundę na ekranie 60 Hz). Zmniejsza to liczbę klatek na sekundę o połowę, ale pozwala zachować spójność animacji. Jak już wspomnieliśmy, nasze oczy są bardziej przystosowane do zmienności niż do liczby klatek na sekundę. Stabilne 30 Hz wygląda lepiej niż 60 Hz z kilkoma klatkami na sekundę.

requestAnimationFrame jest już szeroko omawiany, więc więcej informacji znajdziesz w artykułach takich jak ten z Creative JS. Jest to jednak ważny pierwszy krok do płynnej animacji.

Budżet klatek

Ponieważ chcemy, aby nowa klatka była gotowa przy każdym odświeżeniu ekranu, na wykonanie wszystkich operacji związanych z tworzeniem nowej klatki mamy tylko czas między odświeżeniami. W przypadku wyświetlacza 60 Hz oznacza to, że mamy około 16 ms na wykonanie całego kodu JavaScript, ułożenie elementów, namalowanie i wszystko inne, co przeglądarka musi zrobić, aby wyświetlić ramkę. Oznacza to, że jeśli wykonanie kodu JavaScript w funkcji requestAnimationFrame zajmuje więcej niż 16 ms, nie ma szans na wygenerowanie ramki w czasie, aby można było ją zsynchronizować z synchronizacją pionową.

16 ms to niewiele czasu. Na szczęście narzędzia dla deweloperów w Chrome mogą pomóc Ci wykryć, czy przekroczysz budżet klatek podczas wywołania zwrotnego requestAnimationFrame.

Po otwarciu osi czasu w Narzędziach dla programistów i zapisaniu tej animacji w działaniu szybko widać, że podczas jej tworzenia znacznie przekroczyliśmy budżet. Na osi czasu przejdź do sekcji „Klatki” i sprawdź:

Prezentacja z zbyt dużą ilością układów
Demo z zbyt dużą ilością układów

Te wywołania zwrotne requestAnimationFrame (rAF) zajmują ponad 200 ms. To zbyt długi czas, aby wyświetlać klatkę co 16 ms. Otwarcie jednej z tych długich funkcji zwracających wywołanie zwrotne rAF pozwala zobaczyć, co się dzieje wewnątrz: w tym przypadku jest to wiele elementów układu.

W filmie Paula znajdziesz więcej informacji o przyczynie zmiany układu (czyli o tym, co czytasz na stronie scrollTop) oraz o tym, jak temu zapobiec. Chodzi o to, że możesz dokładniej przyjrzeć się wywołaniu zwrotnemu i sprawdzić, co tak długo trwa.

Zaktualizowane demo z znacznie uproszczonym układem
Zaktualizowana wersja demonstracyjna z znacznie uproszczonym układem

Zwróć uwagę na czas renderowania klatki wynoszący 16 ms. To puste miejsce w ramkach to przestrzeń, w której możesz wykonać więcej pracy (lub pozwolić przeglądarce na wykonanie pracy w tle). Puste miejsce jest dobre.

Inne źródło Jank

Najczęstszą przyczyną problemów podczas uruchamiania animacji opartych na JavaScript jest to, że inne elementy mogą przeszkadzać w wywołaniu funkcji zwracającej wywołanie zwrotne RAF, a nawet uniemożliwić jej uruchomienie. Nawet jeśli wywołanie zwrotne funkcji rAF jest zwięzłe i działa zaledwie przez kilka milisekund, inne działania (takie jak przetwarzanie właśnie otrzymanego zapytania XHR, uruchamianie obsługi zdarzeń wejściowych czy uruchamianie zaplanowanych aktualizacji według zegara) mogą nagle się pojawić i działać przez dowolny czas bez zwracania. Na urządzeniach mobilnych przetwarzanie tych zdarzeń może zająć setki milisekund, w których czasie animacja jest całkowicie wstrzymana. Nazywamy je jank.

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

  • Nie wykonuj zbyt wielu operacji przetwarzania w obsługach danych wejściowych. Wykonywanie dużej ilości operacji JS lub próba przearanżowania całej strony podczas działania np. uchwytu onscroll jest bardzo częstą przyczyną szarpania.
  • Przesuń jak najwięcej operacji przetwarzania (czyli wszystkie, które zajmują dużo czasu) do funkcji wywołania zwrotnego RAF lub Web Workers.
  • Jeśli do wywołania zwrotnego RAF przesyłasz zadania, podziel je na części, aby przetwarzać tylko niewielką część każdej klatki, lub opóźnij je do czasu zakończenia ważnej animacji. Dzięki temu możesz nadal uruchamiać krótkie wywołania zwrotne RAF i płynnie animować.

Wspaniałe samouczek na temat przesuwania przetwarzania do wywołań zwrotnych requestAnimationFrame zamiast do obsługi zdarzeń wejściowych znajdziesz w artykule Paula Lewisa Leaner, Meaner, Faster Animations with requestAnimationFrame (ang. „Lepsze, złośliwsze i szybsze animacje za pomocą requestAnimationFrame”).

Animacja CSS

Co jest lepsze niż lekka wersja JS w przypadku wywołań zwrotnych zdarzeń i raf? Nie ma JS.

Wcześniej wspomnieliśmy, że nie ma uniwersalnego rozwiązania, które pozwoliłoby uniknąć przerywania wywołań zwrotnych rAF, ale możesz użyć animacji CSS, aby całkowicie uniknąć tej potrzeby. W Chrome na Androida (i w innych przeglądarkach, które pracują nad podobnymi funkcjami) animacje CSS mają bardzo pożądaną właściwość, która pozwala przeglądarce często uruchamiać je nawet wtedy, gdy działa JavaScript.

W powyższym rozdziale dotyczącym zacięcia jest zawarte niejawne stwierdzenie: przeglądarki mogą wykonywać tylko jedną czynność naraz. Nie jest to do końca prawda, ale jest to przydatne założenie: w każdej chwili przeglądarka może wykonywać tylko jedną z tych czynności: uruchamiać JS, wykonywać układ lub rysować. Można to sprawdzić w widoku osi czasu w Narzędziach dla programistów. Jednym z wyjątków od tej reguły są animacje CSS w Chrome na Androida (i wkrótce w Chrome na komputery, choć nie jest to jeszcze możliwe).

W miarę możliwości korzystaj z animacji CSS, ponieważ upraszcza to aplikację i pozwala na płynne działanie animacji nawet podczas działania 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);

Gdy klikniesz przycisk, JavaScript będzie działać przez 180 ms, co spowoduje zakłócenia. Jeśli jednak zamiast tego użyjemy animacji CSS, nie będzie już żadnych zakłóceń.

(w momencie pisania tego tekstu animacja CSS nie miała problemów tylko w Chrome na Androida, a nie w wersji 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 animowanych elementach CSS znajdziesz w artykułach takich jak ten na stronie MDN.

Podsumowanie

Krótko mówiąc:

  1. Podczas tworzenia animacji ważne jest generowanie klatek na każde odświeżanie ekranu. Synchronizacja pionowa animacji ma ogromny wpływ na wrażenia użytkownika.
  2. Najlepszym sposobem na uzyskanie synchronizacji pionowej animacji w Chrome i innych nowoczesnych przeglądarkach jest użycie animacji CSS. Jeśli potrzebujesz większej elastyczności niż zapewnia animacja CSS, najlepszym rozwiązaniem jest animacja oparta na metodzie requestAnimationFrame.
  3. Aby animacje RAF działały prawidłowo, zadbaj o to, aby inne metody obsługi zdarzeń nie przeszkadzały w wykonywaniu wywołań zwrotnych RAF, a same wywołania były krótkie (< 15 ms).

Na koniec warto wspomnieć, że animacja z synchronizacją pionową nie dotyczy tylko prostych animacji interfejsu użytkownika. Dotyczy ona też animacji Canvas2D, animacji WebGL, a nawet przewijania na statycznych stronach. W następnym artykule z tej serii omówimy wydajność podczas przewijania z uwzględnieniem tych koncepcji.

Miłej animacji!

Pliki referencyjne