Eliminowanie przerw w celu zwiększenia wydajności renderowania

Tom Wiltzius
Tom Wiltzius

Wprowadzenie

Chcesz, aby aplikacja internetowa działała elastycznie i płynnie podczas tworzenia animacji, przejść i innych drobnych efektów interfejsu. Zadbaj o to, aby nie zakłócały one działania aplikacji. lub niezgrabne, niedoskonałe.

To jest pierwszy z serii artykułów na temat optymalizacji wydajności renderowania w przeglądarce. Na początek wyjaśnimy, dlaczego płynna animacja jest trudna i co trzeba zrobić, aby ją osiągnąć. Zapoznamy się też z kilkoma prostymi sprawdzonymi metodami. Wiele z tych pomysłów przedstawiliśmy pierwotnie w „Jank Busters”, wykład, który w tym roku przeprowadziłem z Natem Ducą podczas konferencji Google I/O (film).

Przedstawiamy synchronizację V

Gracze PC mogą znać to pojęcie, ale w internecie rzadko się ono pojawia, czyli czym jest v-sync?

Weź pod uwagę wyświetlacz telefonu: odświeża się regularnie, zwykle (ale nie zawsze) około 60 razy na sekundę. Synchronizacja pionowa lub pionowa oznacza generowanie nowych klatek tylko między odświeżeniami ekranu. Można to porównać do wyścigu między procesem, który zapisuje dane w buforze ekranu, a systemem operacyjnym odczytującym te dane, aby umieścić je na ekranie. Chcemy, aby zawartość buforowanej ramki zmieniała się między odświeżeniami, a nie podczas nich. W przeciwnym razie na ekranie wyświetli się połowa jednej klatki i połowa drugiej, co spowoduje „rozerwanie”.

Aby uzyskać płynną animację, przy każdym odświeżeniu ekranu musisz dodać nową klatkę. Ma to 2 duże konsekwencje: czas renderowania klatki (czyli czas, przed jakim klatka musi być gotowa) i budżet klatki (czyli czas potrzebny przeglądarce na wygenerowanie klatki). Masz tylko odstęp czasowy między odświeżeniami ekranu (ok. 16 ms w przypadku ekranu 60 Hz), a następną klatkę chcesz rozpocząć zaraz po tym, jak ostatnia klatka zostanie wyświetlona na ekranie.

Czas to wszystko: requestAnimationFrame

Wielu programistów stron internetowych używa do tworzenia animacji setInterval lub setTimeout co 16 milisekund. Jest to problem z wielu powodów (które omówimy bardziej szczegółowo za chwilę), a szczególnie z nich:

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

Przypomnij sobie wspomniany wyżej problem z czasem renderowania klatki: aby przygotować się do kolejnego odświeżenia ekranu, potrzebujesz ukończonej klatki animacji, kodu JavaScript, manipulacji DOM, układu, obrazu itd. Niska rozdzielczość licznika czasu może utrudnić wykonanie klatek animacji przed kolejnym odświeżeniem ekranu, ale wahania częstotliwości odświeżania ekranu uniemożliwiają zastosowanie stałego licznika. Bez względu na to, jaki będzie interwał minutnika, powoli wychodzisz z okna czasowego dla jednej klatki i zrzucasz ją. Zdarza się to nawet wtedy, gdy stoper uruchomił się z dokładnością do milisekundy, a tak się nie stało (jak to wykryli) – rozdzielczość minutnika różni się w zależności od tego, czy urządzenie jest podłączone do zasilania czy podłączone do zasilania. Karty w tle zatrzymują zasoby itp. Nawet jeśli zdarza się to rzadko (np. co 16 klatek z powodu wyłączenia o milisekundę), powoduje to wyświetlenie kilku klatek: Będziesz też wykonywać zadania związane z generowaniem ramek, które nigdy się nie wyświetlają, co zużywa energię i czas pracy procesora, który możesz przeznaczyć na inne działania w aplikacji.

Różne wyświetlacze mają różną częstotliwość odświeżania: częstotliwość odświeżania 60 Hz jest powszechna, ale w niektórych telefonach jest to 59 Hz, w niektórych laptopach w trybie oszczędzania energii pojawia się częstotliwość 50 Hz, a w innych na 70 Hz.

Oceniając wydajność renderowania, skupiamy się zwykle na klatkach na sekundę, ale różnice mogą być jeszcze większym problemem. Nasze oczy dostrzegają w animacji maleńkie, nieregularne zacięcia, które można uzyskać w niewłaściwie zaplanowanej animacji.

Aby uzyskać prawidłowe klatki animacji w określonym czasie, użyj narzędzia requestAnimationFrame. Gdy używasz tego interfejsu API, pytasz przeglądarkę o klatkę animacji. Wywołanie zwrotne jest wywoływane, gdy przeglądarka wkrótce wygeneruje nową klatkę. Dzieje się tak niezależnie od częstotliwości odświeżania.

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

  • Animacje na kartach w tle są wstrzymywane, co pozwala oszczędzać zasoby systemu i baterię.
  • Jeśli system nie jest w stanie obsłużyć renderowania z częstotliwością odświeżania ekranu, może ograniczać animacje i rzadziej wywoływać wywołanie zwrotne (np. 30 razy na sekundę na ekranie o częstotliwości 60 Hz). Zmniejsza to liczbę klatek o połowę, ale zachowuje spójność animacji. Jak wspomnieliśmy wyżej, nasze oczy bardziej zwracają uwagę na wariancję niż na liczbę klatek. Stabilne 30 Hz wygląda lepiej niż 60 Hz, przy którym brakuje kilku klatek na sekundę.

Pole requestAnimationFrame zostało już omówione w wielu miejscach, więc więcej informacji na ten temat znajdziesz w artykułach na ten temat. Pamiętaj jednak, że to ważny pierwszy krok do płynnej animacji.

Budżet ramki

Chcemy, aby po każdym odświeżeniu ekranu była widoczna nowa klatka, więc przerwa między odświeżeniami jest ograniczona. W przypadku wyświetlacza z częstotliwością odświeżania 60 Hz mamy czas około 16 ms na uruchomienie JavaScriptu, utworzenie układu, wyrenderowanie obrazu i inne czynności, jakie musi wykonać przeglądarka, aby pozbyć się ramki. Oznacza to, że jeśli uruchomienie kodu JavaScript w wywołaniu zwrotnym requestAnimationFrame trwa dłużej niż 16 ms, nie masz nadziei, że uda się uzyskać ramkę w czasie do synchronizacji v.

16 ms to niewiele. Narzędzia dla programistów w Chrome mogą pomóc Ci określić, czy zawyżasz budżet klatek w wywołaniu zwrotnym requestAnimationFrame.

Otwarcie osi czasu Narzędzi deweloperskich i nagranie tej animacji w praktyce pokazuje, że podczas tworzenia animacji budżet został przekroczony. Na osi czasu przełącz na „Klatki”. i spójrz na:

Wersja demonstracyjna ze zbyt obszernym układem
Wersja demonstracyjna ze zbyt obszernym układem

Te wywołania zwrotne requestAnimationFrame (rAF) trwają ponad 200 ms. To o rzędu wielkości zbyt długie, aby zaznaczyć klatkę co 16 ms. Otwarcie jednego z tych długich wywołań zwrotnych RAF pokazuje, co się dzieje w tym przypadku: w tym przypadku jest to duży układ.

W filmie Pawła bardziej szczegółowo omawiamy konkretną przyczynę przekaźnika (czytamy: scrollTop) i sposoby jego unikania. Ale tu chodzi o to, że możesz zagłębić się w oddzwonienie i sprawdzić, co tak długo trwa.

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

Zwróć uwagę na czas renderowania klatki wynoszący 16 ms. Pusta przestrzeń w ramkach to przestrzeń na nadgarstek, którą musisz wykonać (lub pozwolić przeglądarce na wykonywanie swoich zadań w tle). To puste miejsce to coś dobrego.

Inne źródło Janka

Największa przyczyna problemów z uruchamianiem animacji opartych na języku JavaScript że inne czynniki mogą przeszkodzić w wywołaniu zwrotnym rAF, a nawet uniemożliwić jego przed uruchomieniem. Nawet jeśli wywołanie zwrotne RAF nie jest bezproblemowe i działa już tylko za kilka w milisekundach, inne działania (takie jak przetwarzanie właśnie przychodzącego XHR, uruchomionych modułów obsługi zdarzeń wejściowych lub przeprowadzania zaplanowanych aktualizacji minutnika). nagle pojawiają się i działają przez dowolny czas, nie tracąc z oczu. Na telefonie gdy urządzenia przetwarzają te zdarzenia, może to potrwać setki milisekund, W tym czasie animacja zostanie całkowicie wstrzymana. Są to tzw. pojawia się zacinanie.

Nie ma magii, jak uniknąć takich sytuacji, ale mamy kilka sprawdzonych metod tworzenia architektury, które pomogą Ci osiągnąć sukces:

  • Wykonuj duże ilości przetwarzania w modułach obsługi danych wejściowych. Częste wykonywanie kodu JS lub próby zmiany kolejności całej strony, np. moduł obsługi onscroll jest bardzo częstą przyczyną problemów.
  • Przekaż jak najwięcej przetwarzania (odczyt: wszystkie działania, których realizacja będzie długotrwała) do wywołania zwrotnego rAF lub środowisk roboczych.
  • Jeśli przekazujesz pracę do wywołania zwrotnego RAF, spróbuj podzielić ją na fragmenty, aby przetwarzać tylko niewielką część każdej klatki, lub opóźnić ją do momentu zakończenia ważnej animacji. Dzięki temu możesz kontynuować krótkie wywołania zwrotne RAF i płynnie animować je.

Świetny samouczek wyjaśniający, jak przekazywać przetwarzanie 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.

Animacja CSS

Czy jest coś lepszego od uproszczonego kodu JavaScript w wywołaniach zwrotnych zdarzeń i rAF? Bez kodu JS.

Wcześniej wspomnieliśmy, że nie ma uniwersalnego sposobu na uniknięcie przerywania wywołań zwrotnych rAF, ale możesz użyć animacji CSS, aby całkowicie tego uniknąć. Szczególnie w Chrome na Androida (i innych przeglądarkach pracujących nad podobnymi funkcjami) animacje CSS mają bardzo pożądaną właściwość, którą często może uruchamiać przeglądarka, nawet gdy jest uruchomiony JavaScript.

W powyższej sekcji o problemie z zaintrygowaniem padło jedno niejawne stwierdzenie: przeglądarki potrafią robić tylko jedną rzecz w danym momencie. Nie jest to prawda do końca, ale warto mieć na uwadze, że w danym momencie przeglądarka może uruchamiać JS, wykonywać układy lub malować obrazy, ale tylko jeden naraz. Możesz to sprawdzić w Narzędziach deweloperskich w widoku Oś czasu. 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, ale jeszcze nie).

Użycie animacji CSS w miarę możliwości upraszcza aplikację i umożliwia płynne wyświetlanie 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);

Jeśli klikniesz przycisk, JavaScript będzie uruchamiany przez 180 ms, powodując zacinanie. Jeśli jednak zastosujemy animację CSS, nie będzie już słychać zacinania.

(Pamiętaj, że w momencie tworzenia 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 artykułach takich jak na stronie MDN.

Podsumowanie

W skrócie:

  1. W przypadku animacji ważne jest, aby przy każdym odświeżeniu ekranu utworzyć klatki. Animacja Vsync ma ogromny pozytywny wpływ na odbiór aplikacji.
  2. Najlepszym sposobem na uzyskanie animacji vsync w Chrome i innych nowoczesnych przeglądarkach jest do animacji CSS. Gdy potrzebujesz większej elastyczności niż animacje CSS najlepsza metoda to animacja oparta na requestAnimationFrame.
  3. Aby zapewnić prawidłowe działanie animacji RAF, upewnij się, że inne moduły obsługi zdarzeń nie przeszkadzają w działaniu wywołania zwrotnego rAF i zachowują wywołania zwrotne rAF (<15 ms).

I wreszcie, animacja vsync ma zastosowanie nie tylko w przypadku prostych animacji interfejsu użytkownika – dotyczy to animacji Canvas2D, animacji WebGL, a nawet przewijania na stronach statycznych. W następnym artykule z tej serii dowiesz się więcej o skuteczności przewijania, biorąc pod uwagę te kwestie.

Miłego tworzenia animacji!

Pliki referencyjne