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 o 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 (film).
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żesz to sobie wyobrazić jako warunek wyścigu między procesem zapisującym dane do bufora ekranu a systemem operacyjnym odczytującym te dane, aby wyświetlić je 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 „rozrywanie”.
Aby uzyskać płynną animację, musisz mieć nową klatkę gotową do użycia za każdym razem, gdy nastąpi odświeżenie ekranu. Ma to 2 ważne konsekwencje: czas wyświetlania klatki (czyli czas, w którym ma być gotowa) i budżet klatek (czyli czas, jaki ma przeglądarka 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 utracą Państwo okno czasowe klatki, co spowoduje jej pominięcie. Wystąpiłoby to nawet wtedy, gdyby zegar działał z dokładnością do milisekundy, co nie jest możliwe (jak odkryli deweloperzy) – rozdzielczość zegara zależy od tego, czy urządzenie jest podłączone do zasilania, czy działa na baterii, i może być modyfikowana przez karty w tle, które pochłaniają zasoby. 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 rzadziej (np. 30 razy na sekundę na ekranie 60 Hz). Zmniejsza to częstotliwość wyświetlania obrazu o połowę, ale pozwala zachować spójność animacji. Jak już wspomnieliśmy, nasze oczy są bardziej przystosowane do zmienności niż do częstotliwości. Stabilne 30 Hz wyglądają lepiej niż 60 Hz z brakiem kilku klatek 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 tworzenie 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 klatki na czas, 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 animacji znacznie przekroczyliśmy budżet. Na osi czasu kliknij „Kadry” i sprawdź:

Te wywołania zwrotne requestAnimationFrame (rAF) zajmują ponad 200 ms. To zbyt długi czas, aby wyświetlać klatkę co 16 ms. Otwarcie jednego z tych długich wywołań zwrotnych funkcji 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.

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). To puste miejsce jest dobre.
Inne źródło Jank
Najczęstszą przyczyną problemów podczas uruchamiania animacji obsługiwanych przez 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 zmiany układu całej strony np. w obiekcie onscroll handler to bardzo częsta przyczyna brzydkich zacięć.
- Przesuń jak najwięcej operacji przetwarzania (czyli wszystkie, które będą długo trwać) do funkcji wywołania zwrotnego rAF lub Web Workers.
- Jeśli chcesz przekazać zadanie do wywołania zwrotnego RAF, podziel je na części, aby przetwarzać tylko niewielką część każdej klatki, lub opóźnij je do czasu, gdy ważna animacja się zakończy. Dzięki temu możesz nadal uruchamiać krótkie wywołania zwrotne RAF i płynnie animować.
Wspaniałe samouczek, w którym Paul Lewis pokazuje, jak przesunąć przetwarzanie do wywołań zwrotnych requestAnimationFrame zamiast do elementów obsługi danych wejściowych, znajdziesz w artykule Leaner, Meaner, Faster Animations with requestAnimationFrame (ang. „Lepsze, skuteczniejsze i szybsze animacje za pomocą metody requestAnimationFrame”).
Animacja CSS
Co jest lepsze niż lekka wersja JS w przypadku wywołań zwrotnych zdarzeń i raf? Nie ma JS.
Wcześniej pisaliśmy, że nie ma uniwersalnego rozwiązania, które pozwoliłoby uniknąć przerywania wywołań zwrotnych funkcji rAF, ale możesz użyć animacji w 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 wyświetlać je nawet wtedy, gdy działa JavaScript.
W powyżej opisanym fragmencie dotyczącym płynności zawarte jest 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 malowanie. 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łócenie. 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 animacji CSS znajdziesz w artykułach takich jak ten na stronie MDN.
Podsumowanie
Krótko mówiąc:
- Podczas tworzenia animacji ważne jest generowanie klatek na każde odświeżanie ekranu. Synchronizacja pionowa animacji ma ogromny wpływ na sposób działania aplikacji.
- 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.
- 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 (mniej niż 15 ms).
Na koniec warto wspomnieć, że animacja z synchronizacją pionową nie dotyczy tylko prostych animacji interfejsu użytkownika. Dotyczy ona także 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!