Wprowadzenie
Aplikacja internetowa powinna działać szybko i płynnie podczas wyświetlania animacji, przejść i innych drobnych efektów interfejsu. Zapewnienie, że te efekty nie będą się zacinać, może decydować o tym, czy aplikacja będzie sprawiać wrażenie „natywnej” czy topornej i niedopracowanej.
To pierwszy z serii artykułów o optymalizacji wydajności renderowania w przeglądarce. Na początek omówimy, dlaczego płynna animacja jest trudna do osiągnięcia i co jest potrzebne, aby ją uzyskać, a także przedstawimy kilka prostych sprawdzonych metod. Wiele z tych pomysłów zostało pierwotnie przedstawionych w prezentacji „Jank Busters”, którą Nat Duca i ja wygłosiliśmy na konferencji Google I/O (film) w tym roku.
Przedstawiamy synchronizację pionową
Gracze komputerowi mogą znać ten termin, ale w internecie jest on rzadko spotykany: co to jest v-sync?
Weźmy pod uwagę wyświetlacz telefonu: odświeża się on w regularnych odstępach czasu, zwykle (ale nie zawsze) około 60 razy na sekundę. V-sync (czyli synchronizacja pionowa) odnosi się do generowania nowych klatek tylko między odświeżeniami ekranu. Można to porównać do sytuacji wyścigu między procesem, który zapisuje dane w buforze ekranu, a systemem operacyjnym, który odczytuje te dane, aby wyświetlić je na ekranie. Chcemy, aby zawartość buforowanej klatki zmieniała się między odświeżeniami, a nie w ich trakcie. W przeciwnym razie monitor wyświetli połowę jednej klatki i połowę drugiej, co doprowadzi do „rozrywania”.
Aby uzyskać płynną animację, przy każdym odświeżeniu ekranu musi być gotowa nowa klatka. Ma to 2 ważne konsekwencje: czas renderowania klatki (czyli czas, w którym klatka musi być gotowa) i budżet klatki (czyli czas, w którym przeglądarka musi wygenerować klatkę). Na wyrenderowanie klatki masz tylko czas między odświeżeniami ekranu (ok. 16 ms na ekranie 60 Hz), a następną klatkę chcesz zacząć tworzyć, gdy tylko ostatnia pojawi się na ekranie.
Wybór właściwego momentu to klucz do sukcesu: requestAnimationFrame
Wielu programistów stron internetowych używa funkcji setInterval lub setTimeout co 16 milisekund do tworzenia animacji. Jest to problem z wielu powodów (które omówimy za chwilę), ale szczególnie niepokojące są:
- Rozdzielczość zegara w języku JavaScript wynosi tylko kilka milisekund.
- Różne urządzenia mają różne częstotliwości odświeżania
Przypomnij sobie wspomniany powyżej problem z czasem renderowania klatki: musisz mieć gotową klatkę animacji, w której zakończono wszystkie operacje JavaScriptu, manipulacje DOM, układ, malowanie itp., zanim nastąpi kolejne odświeżenie ekranu. Niska rozdzielczość zegara może utrudniać ukończenie klatek animacji przed następnym odświeżeniem ekranu, ale różnice w częstotliwości odświeżania ekranu uniemożliwiają to przy stałym zegarze. Niezależnie od interwału czasomierza, powoli będziesz oddalać się od okna czasowego klatki i w końcu ją upuścisz. Dzieje się tak nawet wtedy, gdy licznik czasu działa z dokładnością do milisekundy, co nie jest możliwe (jak odkryli deweloperzy) – rozdzielczość licznika czasu różni się w zależności od tego, czy urządzenie jest zasilane z baterii czy z sieci, może na nią wpływać obciążenie zasobów przez karty w tle itp. Nawet jeśli zdarza się to rzadko (powiedzmy co 16 klatek, ponieważ zabrakło milisekundy), zauważysz to: będziesz tracić kilka klatek na sekundę. Będziesz też wykonywać pracę związaną z generowaniem klatek, które nigdy nie zostaną wyświetlone, co marnuje energię i czas pracy procesora, który można by poświęcić na inne działania 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 spadają do 50 Hz w trybie oszczędzania baterii, a niektóre monitory komputerów stacjonarnych mają 70 Hz.
Podczas omawiania wydajności renderowania zwykle skupiamy się na liczbie klatek na sekundę (FPS), ale wariancja może być jeszcze większym problemem. Nasze oczy zauważają drobne, nieregularne zakłócenia, które mogą być spowodowane przez źle zsynchronizowaną animację.
Aby uzyskać klatki animacji o odpowiednim czasie, użyj requestAnimationFrame. Gdy używasz tego interfejsu API, prosisz przeglądarkę o klatkę animacji. Wywołanie zwrotne jest wywoływane, gdy przeglądarka ma wkrótce wygenerować nową klatkę. 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 systemowe i baterię.
- Jeśli system nie może obsłużyć renderowania z częstotliwością odświeżania ekranu, może ograniczyć animacje i rzadziej wywoływać wywołanie zwrotne (np. 30 razy na sekundę na ekranie 60 Hz). Chociaż zmniejsza to liczbę klatek na sekundę o połowę, pozwala zachować spójność animacji. Jak wspomniano powyżej, nasze oczy są znacznie bardziej wyczulone na zmiany niż na liczbę klatek na sekundę. Stałe 30 Hz wygląda lepiej niż 60 Hz, gdy brakuje kilku klatek na sekundę.
requestAnimationFrame został już omówiony w wielu miejscach, więc więcej informacji na ten temat znajdziesz w artykułach takich jak ten z Creative JS. Jest to jednak ważny pierwszy krok do płynnej animacji.
Budżet klatek
Chcemy, aby przy każdym odświeżeniu ekranu była gotowa nowa klatka, więc na wykonanie wszystkich czynności związanych z jej utworzeniem mamy tylko czas między odświeżeniami. Na wyświetlaczu 60 Hz oznacza to, że mamy około 16 ms na uruchomienie całego kodu JavaScript, wykonanie układu, renderowanie i wszystkie inne czynności, które przeglądarka musi wykonać, aby wyświetlić klatkę. Oznacza to, że jeśli wykonanie kodu JavaScript w wywołaniu zwrotnym requestAnimationFrame zajmie więcej niż 16 ms, nie ma szans na wygenerowanie klatki na czas synchronizacji pionowej.
16 ms to niewiele czasu. Na szczęście narzędzia dla programistów w Chrome mogą pomóc Ci sprawdzić, czy przekraczasz budżet klatek podczas wywołania zwrotnego requestAnimationFrame.
Otwarcie osi czasu w Narzędziach dla programistów i nagranie tej animacji w działaniu szybko pokazuje, że podczas animowania znacznie przekraczamy budżet. Na osi czasu przełącz się na „Klatki” i spójrz:
Te wywołania zwrotne requestAnimationFrame (rAF) trwają ponad 200 ms. To o rząd wielkości za długo, aby wyświetlać klatkę co 16 ms. Otwarcie jednego z tych długich wywołań zwrotnych rAF ujawnia, co dzieje się wewnątrz – w tym przypadku jest to dużo układu.
W filmie Paula znajdziesz więcej informacji o konkretnej przyczynie zmiany układu (odczyt scrollTop) i o tym, jak jej uniknąć. Chodzi o to, że możesz przeanalizować wywołanie zwrotne i sprawdzić, co zajmuje tyle czasu.
Zwróć uwagę na czas renderowania klatki wynoszący 16 ms. Puste miejsce w ramkach to przestrzeń, którą możesz wykorzystać na dodatkowe zadania (lub pozwolić przeglądarce na wykonanie pracy w tle). To puste miejsce to dobra rzecz.
Inne źródło zacinania się
Najczęstszą przyczyną problemów podczas prób uruchomienia animacji opartych na języku JavaScript jest to, że inne elementy mogą przeszkadzać w wywołaniu zwrotnym rAF, a nawet uniemożliwić jego uruchomienie. Nawet jeśli wywołanie zwrotne rAF jest proste i wykonuje się w ciągu kilku milisekund, inne działania (takie jak przetwarzanie przychodzącego żądania XHR, uruchamianie modułów obsługi zdarzeń wejściowych lub uruchamianie zaplanowanych aktualizacji w liczniku czasu) mogą nagle się pojawić i wykonywać przez dowolny czas bez przerwy. Na urządzeniach mobilnych przetwarzanie tych zdarzeń może czasami zająć setki milisekund, podczas których animacja będzie całkowicie zatrzymana. Takie problemy z animacją nazywamy zacinaniem się.
Nie ma magicznego sposobu na uniknięcie takich sytuacji, ale istnieje kilka sprawdzonych metod dotyczących architektury, które pomogą Ci osiągnąć sukces:
- Nie wykonuj zbyt wielu operacji w modułach obsługi danych wejściowych. Wykonywanie wielu operacji JS lub próba zmiany układu całej strony np. podczas obsługi zdarzenia onscroll to bardzo częsta przyczyna poważnych problemów z płynnością.
- Przenieś jak najwięcej operacji (czyli wszystko, co zajmuje dużo czasu) do wywołania zwrotnego rAF lub do Web Workers.
- Jeśli przeniesiesz pracę do wywołania zwrotnego rAF, spróbuj podzielić ją na mniejsze części, aby w każdej klatce przetwarzać tylko niewielką ilość danych, lub opóźnij ją do momentu zakończenia ważnej animacji. W ten sposób możesz nadal uruchamiać krótkie wywołania zwrotne rAF i płynnie animować.
Świetny samouczek dotyczący przenoszenia przetwarzania do wywołań zwrotnych requestAnimationFrame zamiast do modułów obsługi danych wejściowych znajdziesz w artykule Paula Lewisa Leaner, Meaner, Faster Animations with requestAnimationFrame (Lżejsze, lepsze i szybsze animacje dzięki requestAnimationFrame).
Animacja CSS
Co jest lepsze niż lekki kod JS w wywołaniach zwrotnych zdarzeń i rAF? Bez JS.
Wcześniej wspomnieliśmy, że nie ma jednego uniwersalnego sposobu na uniknięcie przerywania wywołań zwrotnych rAF, ale możesz użyć animacji CSS, aby całkowicie wyeliminować potrzebę ich stosowania. W Chrome na Androida (inne przeglądarki pracują nad podobnymi funkcjami) animacje CSS mają bardzo pożądaną właściwość – przeglądarka może je często uruchamiać, nawet jeśli działa JavaScript.
W powyższej sekcji dotyczącej zacinania się znajduje się domyślne stwierdzenie: przeglądarki mogą wykonywać tylko jedną czynność naraz. Nie jest to do końca prawda, ale warto przyjąć takie założenie: w danym momencie przeglądarka może wykonywać kod JS, układać elementy lub je renderować, ale tylko jedną z tych czynności naraz. Można to sprawdzić w widoku osi czasu w Narzędziach deweloperskich. Jednym z wyjątków od tej reguły są animacje CSS w Chrome na Androida (a wkrótce także w Chrome na komputery).
Jeśli to możliwe, użycie animacji CSS upraszcza aplikację i zapewnia płynne działanie animacji, nawet gdy działa JavaScript.
// 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);
Jeśli klikniesz przycisk, JavaScript będzie działać przez 180 ms, co spowoduje zacinanie się. Jeśli jednak użyjemy animacji CSS, zacinanie się nie występuje.
(Pamiętaj, że w momencie pisania tego artykułu animacja CSS jest płynna 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 używaniu animacji CSS znajdziesz w artykułach takich jak ten w MDN.
Podsumowanie
Krótko mówiąc:
- Podczas animowania ważne jest, aby generować klatki dla każdego odświeżenia ekranu. Animacja zsynchronizowana z częstotliwością odświeżania ekranu ma ogromny pozytywny wpływ na to, jak aplikacja jest odbierana.
- Najlepszym sposobem na uzyskanie animacji zsynchronizowanej z częstotliwością odświeżania ekranu w Chrome i innych nowoczesnych przeglądarkach jest użycie animacji CSS. Jeśli potrzebujesz większej elastyczności niż ta, którą zapewnia animacja CSS, najlepszą techniką jest animacja oparta na requestAnimationFrame.
- Aby animacje rAF działały prawidłowo, upewnij się, że inne moduły obsługi zdarzeń nie przeszkadzają w wywołaniu zwrotnym rAF, i dbaj o to, aby wywołania zwrotne rAF były krótkie (<15 ms).
Na koniec warto dodać, że animacja zsynchronizowana z częstotliwością odświeżania nie dotyczy tylko prostych animacji interfejsu – obejmuje też animacje Canvas2D, WebGL, a nawet przewijanie stron statycznych. W następnym artykule z tej serii przyjrzymy się wydajności przewijania, mając na uwadze te pojęcia.
Życzymy udanego tworzenia animacji.