Paralaksyna

Wprowadzenie

Strony z efektem paralaksy są ostatnio bardzo popularne. Oto kilka przykładów:

Jeśli nie wiesz, o co chodzi, to są to witryny, w których struktura wizualna strony zmienia się podczas przewijania. Zwykle elementy na stronie są skalowane, obracane lub przesuwane proporcjonalnie do pozycji przewijania na stronie.

Demonstracyjna strona z efektem paralaksy
Nasza strona demonstracyjna z efektem paralaksy

Niezależnie od tego, czy lubisz strony z efektem paralaksy, czy nie, możesz z pewnością stwierdzić, że są one czarną dziurą pod względem wydajności. Wynika to z tego, że przeglądarki są zwykle optymalizowane pod kątem sytuacji, w której podczas przewijania (w zależności od kierunku) nowe treści pojawiają się u góry lub u dołu ekranu. Ogólnie rzecz biorąc, przeglądarki działają najlepiej, gdy podczas przewijania nie zachodzi wiele zmian wizualnych. W przypadku witryn z paralaksą rzadko tak się dzieje, ponieważ często zmieniają się duże elementy wizualne na całej stronie, co powoduje, że przeglądarka musi ponownie przemalować całą stronę.

W przypadku witryn z efektem paralaksy można przyjąć następujące założenia:

  • Elementy tła, które podczas przewijania w górę i w dół zmieniają swoją pozycję, obrót i skalę.
  • Treści strony, takie jak tekst lub mniejsze obrazy, które przewijają się w typowy sposób od góry do dołu.

W poprzednim artykule omawialiśmy wydajność przewijania oraz sposoby na poprawę szybkości reakcji aplikacji. Ten artykuł opiera się na tych informacjach, więc warto go przeczytać, jeśli jeszcze tego nie zrobiłeś.

Pytanie brzmi: jeśli tworzysz witrynę z paralaksą, czy musisz stosować drogie metody odświeżania, czy też istnieją inne sposoby na maksymalizację skuteczności? Przyjrzyjmy się dostępnym opcjom.

Opcja 1. Użyj elementów DOM i pozycji bezwzględnych

Wygląda na to, że jest to domyślne podejście większości osób. Na stronie jest wiele elementów, a za każdym razem, gdy zostanie wywołane zdarzenie przewijania, wprowadza się wiele zmian wizualnych, aby je przekształcić.

Jeśli uruchomisz pasek czasu w DevTools w trybie ramki i przewiniesz stronę, zauważysz kosztowne operacje malowania na pełnym ekranie. Jeśli przewiniesz dużo, możesz zobaczyć kilka zdarzeń przewijania w ramce, z których każde spowoduje wykonanie operacji układu.

Narzędzia deweloperskie Chrome bez debouncedowanych zdarzeń przewijania.
Narzędzia deweloperskie wyświetlające duże operacje malowania i wiele układów wywoływanych zdarzeniami w pojedynczym kadrze.

Pamiętaj, że aby osiągnąć 60 FPS (co odpowiada typowej częstotliwości odświeżania monitora 60 Hz), masz tylko 16 ms na wykonanie wszystkich czynności. W tej pierwszej wersji aktualizacje wizualne są wykonywane za każdym razem, gdy otrzymamy zdarzenie przewijania, ale jak wspomnieliśmy w poprzednich artykułach na temat zoptymalizowanych animacji z użyciem requestAnimationFramewydajności przewijania, nie jest to zgodne z harmonogramem aktualizacji przeglądarki, więc albo pomijamy klatki, albo wykonujemy zbyt dużo pracy w każdej z nich. Może to spowodować nienaturalne działanie witryny, co zniechęci użytkowników i spowoduje niezadowolenie kotów.

Przesuń kod aktualizacji ze zdarzenia przewijania do wywołania zwrotnego requestAnimationFrame i po prostu uchwyć wartość przewijania w wywołaniu zwrotnym zdarzenia przewijania.

Jeśli powtórzysz test przewijania, być może zauważysz niewielką poprawę, ale niezbyt dużą. Dzieje się tak, ponieważ operacja układu, którą uruchamiamy przez przewijanie, nie jest aż tak kosztowna, ale w innych przypadkach może być. Teraz w każdej ramce wykonujemy co najmniej jedną operację układu.

Narzędzia deweloperskie w Chrome z zdejmowaniem drgań zdarzeń przewijania.
Narzędzia deweloperskie wyświetlające duże operacje malowania i wiele układów wywoływanych zdarzeniami w pojedynczym kadrze.

Możemy teraz obsługiwać jedno lub sto zdarzeń przewijania na każdy kadr, ale przechowujemy tylko najnowszą wartość, aby używać jej podczas wywołania funkcji requestAnimationFrame, która wykonuje aktualizacje wizualne. Chodzi o to, że zamiast próbować wymuszać aktualizacje wizualne za każdym razem, gdy otrzymasz zdarzenie przewijania, prosisz przeglądarkę o przekazanie odpowiedniego okna, w którym można je wykonać. Niezły z Ciebie słodziak.

Głównym problemem związanym z tym podejściem jest to, że requestAnimationFrame mamy tylko jedną warstwę dla całej strony, a przesuwanie tych elementów wizualnych wymaga dużych (i drogich) przemalowań. Zazwyczaj malowanie jest operacją blokującą (chociaż to się zmienia), co oznacza, że przeglądarka nie może wykonywać żadnych innych zadań. Często przekraczamy też budżet na ramkę wynoszący 16 ms, przez co obraz jest niestabilny.

Opcja 2. Użyj elementów DOM i przekształceń 3D

Zamiast stosowania bezwzględnych pozycji możemy też zastosować do elementów transformacje 3D. W tej sytuacji elementy z zastosowaniem transformacji 3D mają nową warstwę na każdy element, a w przeglądarkach WebKit często powoduje to też przełączenie na kompozytor sprzętowy. W przypadku opcji 1 mieliśmy jedną dużą warstwę strony, którą trzeba było ponownie namalować, gdy cokolwiek się zmieniło, a wszystkie operacje malowania i kompozycji były wykonywane przez procesor.

Oznacza to, że w przypadku tej opcji jest inaczej: mamy potencjalnie jedną warstwę dla każdego elementu, do którego stosujemy transformację 3D. Jeśli od tego momentu będziemy wykonywać tylko przekształcenia elementów, nie będziemy musieli ponownie rysować warstwy, a GPU będzie mogło zajmować się przemieszczaniem elementów i kompozycją końcowej strony.

Często użytkownicy po prostu korzystają z hacka -webkit-transform: translateZ(0); i zauważają magiczną poprawę wydajności. Chociaż to działa, występują pewne problemy:

  1. Nie jest ona zgodna z innymi przeglądarkami.
  2. Wymusza ona działanie przeglądarki przez utworzenie nowej warstwy dla każdego przekształconego elementu. Wiele warstw może powodować inne wąskie gardła w sprawie wydajności, więc używaj ich oszczędnie.
  3. Został wyłączony w przypadku niektórych portów WebKit (czwarty punkt od dołu).

Jeśli zdecydujesz się na tłumaczenie 3D, zachowaj ostrożność, ponieważ jest to tymczasowe rozwiązanie. W idealnej sytuacji transformacje 2D miałyby podobne właściwości renderowania co transformacje 3D. Przeglądarki rozwijają się w bardzo szybkim tempie, więc miejmy nadzieję, że uda nam się to zrobić wcześniej.

Na koniec warto unikać stosowania funkcji malowania, jeśli to możliwe, i po prostu przenosić istniejące elementy na stronie. Na przykład w witrynach z paralaksą często stosuje się bloki div o stałej wysokości i zmienia się ich położenie w tle, aby uzyskać efekt paralaksy. Oznacza to, że element musi być ponownie malowany przy każdym przejściu, co może mieć negatywny wpływ na wydajność. Zamiast tego, jeśli to możliwe, utwórz element (w razie potrzeby owiń go w element div za pomocą atrybutu overflow: hidden) i po prostu przetłumacz go.

Opcja 3. Użyj rysunku na stałym miejscu lub WebGL

Ostatnią opcją, którą rozważymy, jest użycie obrazu tła w stałym położeniu na tylnej stronie strony, na którym będziemy rysować przekształcone obrazy. Na pierwszy rzut oka może się wydawać, że to nie jest najbardziej wydajne rozwiązanie, ale ma ono kilka zalet:

  • Nie wymagamy już tak dużo pracy od kompozytora, ponieważ mamy tylko jeden element – płótno.
  • Mamy do czynienia z jedną bitmapą z sprzętową akceleracją.
  • Interfejs Canvas2D API doskonale nadaje się do tego typu przekształceń, co oznacza, że rozwój i konserwacja są łatwiejsze do zarządzania.

Użycie elementu na płótnie tworzy nową warstwę, ale jest to jedna warstwa. W przypadku opcji 2. utworzono nową warstwę dla każdego elementu z zastosowaniem transformacji 3D, więc musimy wykonać więcej pracy, aby połączyć wszystkie te warstwy. Jest to też obecnie najbardziej kompatybilne rozwiązanie, biorąc pod uwagę różne implementacje przekształceń w różnych przeglądarkach.


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

To podejście sprawdza się w przypadku dużych obrazów (lub innych elementów, które można łatwo zapisać na płótnie). Obsługa dużych bloków tekstu jest z pewnością trudniejsza, ale w zależności od witryny może to być najlepsze rozwiązanie. Jeśli musisz pracować z tekstem na kanwie, musisz użyć metody fillText interfejsu API, ale kosztem dostępności (tekst zostanie przetworzony na bitmapę), a teraz musisz się zająć zawijaniem wierszy i całym mnóstwem innych problemów. Jeśli możesz tego uniknąć, zdecydowanie powinieneś to zrobić. Prawdopodobnie lepsze efekty osiągniesz, stosując opisane powyżej przekształcenia.

Ponieważ staramy się, aby wszystko było jak najlepsze, nie ma powodu, aby przypuszczać, że efekt paralaksy powinien być realizowany w ramach elementu na płótnie. Jeśli przeglądarka obsługuje tę funkcję, możemy użyć WebGL. Kluczową kwestią jest to, że WebGL ma najkrótszą drogę do karty graficznej spośród wszystkich interfejsów API, dlatego jest najbardziej prawdopodobnym kandydatem na osiągnięcie 60 FPS, zwłaszcza jeśli efekty w witrynie są złożone.

Twoja pierwsza reakcja może być taka, że WebGL jest zbędny lub nie jest powszechnie obsługiwany, ale jeśli używasz czegoś takiego jak Three.js, zawsze możesz użyć elementu canvas, a Twój kod będzie abstrakcyjny w sposób spójny i przyjazny. Wystarczy, że użyjesz biblioteki Modernizr, aby sprawdzić, czy interfejs API jest obsługiwany:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

Jeśli nie chcesz dodawać dodatkowych elementów do strony, możesz zawsze użyć płótna jako elementu tła w przeglądarce Firefox i w przeglądarkach opartych na WebKit. Oczywiście nie jest to powszechne, więc jak zwykle należy zachować ostrożność.

Wybór należy do Ciebie

Głównym powodem, dla którego deweloperzy domyślnie wybierają elementy z bezwzględnym pozycjonowaniem, a nie inne opcje, może być po prostu powszechna obsługa. W pewnym stopniu jest to iluzja, ponieważ starsze przeglądarki, na które jest kierowana reklama, prawdopodobnie będą miały problemy z renderowaniem. Nawet w przypadku współczesnych przeglądarek korzystanie z elementów z pozycji bezwzględnej niekoniecznie przynosi dobre wyniki.

Transformacje, zwłaszcza 3D, umożliwiają pracę bezpośrednio z elementami DOM i osiągnięcie stabilnej liczby klatek na sekundę. Kluczem do sukcesu jest unikanie malowania, jeśli to możliwe, i proste przenoszenie elementów. Pamiętaj, że sposób tworzenia warstw przez przeglądarki WebKit niekoniecznie jest zgodny z innymi silnikami przeglądarek, więc przed zastosowaniem tego rozwiązania przetestuj je.

Jeśli zależy Ci tylko na obsłudze najpopularniejszych przeglądarek i możesz renderować witrynę za pomocą canvasów, to może być najlepsza opcja. Jeśli używasz Three.js, możesz bardzo łatwo przełączać się między renderowaniem zależnie od wymaganej obsługi.

Podsumowanie

Przeanalizowaliśmy kilka podejść do obsługi witryn z paralaksą, od elementów z pozycjonowaniem bezwzględnym po stosowanie obrazu o stałym położeniu. Sposób implementacji zależy oczywiście od tego, czego chcesz dokonać, i od konkretnego projektu, nad którym pracujesz, ale zawsze warto wiedzieć, jakie masz możliwości.

I jak zawsze, niezależnie od tego, które podejście wybierzesz: nie zgaduj, tylko testuj.