Paralaksyna

Wstęp

Witryny z paralaksą są ostatnio bardzo popularne. Spójrz na te strony:

Jeśli nie znasz tych witryn, są to witryny, w których wizualna struktura strony zmienia się podczas przewijania. Normalnie elementy w skali strony, obracają się lub poruszają proporcjonalnie do miejsca przewijania na stronie.

Strona demonstracyjna paralaksy
Nasza strona demonstracyjna z efektem paralaksy

To, czy lubisz witryny z paralaksą, to jedno, ale możesz śmiało powiedzieć, że są one jak czarną dziurę w skuteczności. Wynika to z tego, że przeglądarki są zwykle optymalizowane pod kątem nowych treści podczas przewijania (w zależności od kierunku przewijania) wyświetlanych u góry lub u dołu ekranu. Ogólnie rzecz biorąc, przeglądarki działają najlepiej, gdy podczas przewijania nic nie zmienia się. W witrynie z paralaksą, która rzadko się zdarza, ponieważ duże elementy wizualne często się zmieniają, co powoduje ponowne wyrenderowanie całej strony przez przeglądarkę.

Rozsądne jest uogólnienie witryny z paralaksą w taki sposób:

  • elementy tła, które podczas przewijania w górę i w dół zmieniają swoje położenie, obrót i skalę;
  • zawartość strony, np. tekst lub mniejsze obrazy, które przesuwa się w typowy sposób od góry do dołu;

Omówiliśmy już wydajność przewijania oraz sposoby poprawy reagowania aplikacji. Ten artykuł opiera się na tych informacjach, więc warto się z nimi zapoznać, jeśli nie znasz jeszcze tej funkcji.

Pytanie brzmi więc, jeśli tworzysz witrynę z funkcją przewijania z paralaksą, czy musisz korzystać z drogich przemalowań, czy możesz zastosować alternatywne podejście, aby zmaksymalizować skuteczność. Przyjrzyjmy się naszym opcjom.

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

Wydaje się, że jest to podejście domyślne większości osób. Na stronie jest kilka elementów, a po każdym wywołaniu zdarzenia przewijania następuje kilka aktualizacji wizualnych w celu ich przekształcenia.

Jeśli uruchomisz oś czasu w Narzędziach deweloperskich w trybie ramki i przewiniesz wokół, zauważysz, że są kosztowne operacje renderowania na pełnym ekranie. Jeśli przewiniesz dużo ekranu, możesz zobaczyć kilka zdarzeń przewijania w ramach jednej klatki, z których każda będzie aktywować układ.

Narzędzia deweloperskie w Chrome bez zdarzeń przewijania z odbiorem.
Narzędzia deweloperskie z dużymi obrazami i wieloma układami wywoływanymi przez zdarzenia w jednej klatce.

Warto pamiętać, że osiągnięcie 60 kl./s (czyli typowej częstotliwości odświeżania monitora 60 Hz) wymaga trochę ponad 16 ms na wykonanie wszystkich zadań. W pierwszej wersji wykonywane są aktualizacje wizualne za każdym razem, gdy pojawia się zdarzenie przewijania, ale jak wspomnieliśmy w poprzednich artykułach o mniejszych i mniejszych animacji z requestAnimationFrame oraz o wydajności przewijania, nie zbiega się to z harmonogramem aktualizacji przeglądarki, więc przegapiamy klatki albo za dużo robimy w każdej z nich. To z łatwością może sprawić, że witryna będzie nienaturalna, a jej treść będzie bardziej nieprzyjazna, a użytkownicy będą rozczarowani i niezadowoleni z kotków.

Przenieśmy kod aktualizacji ze zdarzenia przewijania do wywołania zwrotnego requestAnimationFrame i po prostu przechwyć wartość przewijania w wywołaniu zwrotnym zdarzenia przewijania.

Jeśli powtórzysz test przewijania, możesz zauważyć drobną, ale niedużą poprawę. Powodem jest to, że operacja układu, którą wyzwalamy przez przewijanie, nie jest zbyt kosztowna, ale w innych przypadkach może tak być. Teraz w każdej klatce wykonujemy przynajmniej tylko jedną operację układu.

Narzędzia deweloperskie w Chrome ze zdarzeniami przewijania z wyciszeniem.
Narzędzia deweloperskie z dużymi obrazami i wieloma układami wywoływanymi przez zdarzenia w jednej klatce.

Teraz możemy obsłużyć 100 zdarzeń przewijania na klatkę, ale nadal przechowujemy tylko najnowszą wartość do wykorzystania przy uruchomieniu wywołania zwrotnego requestAnimationFrame i wprowadzaniu aktualizacji wizualnych. Problem polegał na tym, że od wymuszania aktualizacji wizualnych za każdym razem, gdy otrzymasz zdarzenie przewijania, do udostępnienia przez przeglądarkę odpowiedniego okna do ich wykonania. Nieźle, co?

Głównym problemem przy tym podejściu (requestAnimationFrame lub nie) jest to, że mamy w zasadzie jedną warstwę na całą stronę, a przenoszenie tych elementów wizualnych wymaga dużych (i drogich) przemalowań. Zwykle mówienie o obrazie jest operacją blokującą (chociaż zmienia się), co oznacza, że przeglądarka nie może wykonywać żadnych innych zadań i często przekraczamy budżet wynoszący 16 ms, przez co wszystko pozostaje nie tak.

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

Zamiast stosowania pozycji bezwzględnych, możemy zastosować również przekształcenia 3D do elementów. W tej sytuacji widzimy, że elementy z przekształceniami 3D otrzymują na każdy element nową warstwę, a w przeglądarkach WebKit często powoduje to również przełączenie się na kompozytor sprzętowy. Z kolei w opcji 1 mieliśmy jedną dużą warstwę dla strony, która wymagała ponownego wymalowania, gdy cokolwiek się zmieniło, a całą obsługę malowania i komponowania odpowiadał procesor.

Oznacza to, że w przypadku tej opcji sytuacja wygląda inaczej: potencjalnie mamy jedną warstwę na każdy element, do którego stosujemy przekształcenie 3D. Jeśli od tego momentu zrobimy więcej przekształceń elementów, nie będziemy musieli ponownie malować warstwy, a GPU zajmie się przenoszeniem elementów i komponowaniem razem ostatecznej strony.

Nierzadko użytkownicy po prostu korzystają z metody -webkit-transform: translateZ(0); i obserwują magiczną poprawę wydajności, ale obecnie pojawiają się problemy:

  1. Nie działa w różnych przeglądarkach.
  2. Powoduje, że ręka przeglądarki jest wymuszana, tworząc nową warstwę dla każdego przekształconego elementu. Wiele warstw może powodować problemy z wydajnością, więc używaj ich z umiarem.
  3. Została wyłączona dla niektórych portów WebKit (czwarty punkt od dołu).

Zachowaj ostrożność podczas tłumaczenia 3D, ponieważ jest to tymczasowe rozwiązanie Twojego problemu. W idealnej sytuacji dałoby się zaobserwować podobne cechy renderowania po przekształceniu 2D i 3D. Przeglądarki rozwijają się w fenomenalnym tempie, mamy więc nadzieję, że wiemy, co się stało.

Pamiętaj też, by unikać malowania wszędzie tam, gdzie to możliwe, i po prostu przesuwać istniejące elementy w obrębie strony. Na przykład w witrynach z paralaksą zazwyczaj stosuje się elementy div o stałej wysokości i zmienia ich położenie tła, aby osiągnąć odpowiedni efekt. Niestety oznacza to, że trzeba ponownie malować dany element za każdym razem, co może obniżyć wydajność. Zamiast tego utwórz element (w razie potrzeby umieść go w elemencie div za pomocą atrybutu overflow: hidden) i przetłumacz go.

Opcja 3. Użyj kanwy o stałej pozycji lub WebGL

Ostatnią opcją, którą rozważymy, jest użycie obszaru roboczego o stałej pozycji na dole strony, w którym rysujemy przekształcone obrazy. Na pierwszy rzut oka może się wydawać, że nie jest to najbardziej wydajne rozwiązanie, ale właściwie ma kilka zalet:

  • Nie wymagamy już tak dużo pracy kompozytora, ponieważ mamy tylko jeden element – obiekt canvas.
  • W efekcie mamy do czynienia z jedną bitmapą z akceleracją sprzętową.
  • Interfejs Canvas2D API doskonale sprawdza się w przypadku przekształceń, które chcemy przeprowadzić, co oznacza, że programowanie i konserwacja są łatwiejsze w zarządzaniu.

Użycie elementu canvas daje nam nową warstwę, ale jest to tylko jedna warstwa, podczas gdy w opcji 2 faktycznie dostaliśmy nową warstwę dla każdego elementu z przekształceniem 3D, przez co mamy większe nakłady pracy związane z komponowaniem tych warstw. Jest to również najbardziej kompatybilne obecnie rozwiązanie, biorąc pod uwagę różną implementację 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));
}

Ta metoda sprawdza się w przypadku dużych obrazów (lub innych elementów, które można łatwo zapisać w obszarze roboczym). Z pewnością praca z dużymi blokami tekstu jest trudniejsza, ale w zależności od witryny może się okazać najbardziej odpowiednim rozwiązaniem. Jeśli musisz radzić sobie z tekstem w obszarze roboczym, musisz użyć metody interfejsu API fillText, ale wiąże się to z koniecznością ułatwienia dostępu (wystarczy zrastrowanie tekstu w bitmapę). Dzięki temu będziesz mieć teraz do dyspozycji zawijanie wierszy i mnóstwo innych problemów. Jeśli możesz tego uniknąć, naprawdę warto to zrobić, a zastosowanie omówionej metody przekształcania przyniesie Ci większą skuteczność.

Gdy posuniesz się tak daleko, jak to możliwe, nie ma powodu zakładać, że działanie paralaksy powinno być realizowane wewnątrz elementu canvas. Jeśli przeglądarka ją obsługuje, możemy użyć WebGL. Co ważne, WebGL ma najbardziej precyzyjną trasę przez wszystkie interfejsy API do karty graficznej, więc z największym prawdopodobieństwem uzyskasz 60 kl./s, zwłaszcza jeśli efekty strony są złożone.

Natychmiastową reakcją może być fakt, że WebGL nie jest w ogóle dostępny pod względem pomocy, ale jeśli używasz czegoś takiego jak Three.js, zawsze możesz wrócić do elementu canvas, a kod zostanie wyodrębniony w spójny i przyjazny sposób. Wystarczy użyć narzędzia Modernizr, aby znaleźć odpowiednią obsługę interfejsu API:

// 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 przepadasz za dodawaniem dodatkowych elementów do strony, możesz użyć płótna jako elementu tła w przeglądarkach Firefox i WebKit. Nie jest to oczywiście powszechne, więc jak zwykle należy zachować ostrożność.

Wybór należy do Ciebie

Główną przyczyną, dla której deweloperzy zamiast innych opcji są elementy umieszczone w eksperymencie, może być po prostu ich wszechstronność. Jest to do pewnego stopnia zabawne, ponieważ starsze przeglądarki, na które są kierowane reklamy, prawdopodobnie będą miały bardzo słabe wrażenia z renderowania. Nawet w nowoczesnych przeglądarkach, w których korzystanie z absolutnie rozmieszczonych elementów nie musi zapewnić dobrej skuteczności.

Przekształcenia, z pewnością trójwymiarowe, umożliwiają bezpośrednią pracę z elementami DOM i uzyskują stałą liczbę klatek. Kluczem do sukcesu jest nie malowanie wszędzie tam, gdzie się da, i po prostu staraj się przesuwać elementy. Pamiętaj, że sposób tworzenia warstw przez przeglądarki WebKit nie musi być powiązany z innymi silnikami przeglądarek, więc przetestuj to przed podjęciem decyzji o zastosowaniu tego rozwiązania.

Jeśli zależy Ci tylko na najwyższej jakości przeglądarek i możesz renderować witrynę za pomocą obszaru roboczego, może to być dla Ciebie najlepszy wybór. Gdybyście używali Three.js, z pewnością łatwo można przełączać się między mechanizmami renderowania (w zależności od wymaganej pomocy).

Podsumowanie

Oceniliśmy kilka podejścia do postępowania z witrynami z paralaksą, od absolutnie rozmieszczonych elementów po korzystanie z obszaru roboczego o stałym położeniu. Implementacja będzie oczywiście zależała od tego, co chcesz osiągnąć i jakiego projektu używasz, ale zawsze warto wiedzieć, jakie masz możliwości.

I tak jak zawsze – nie zgaduj – przetestuj ją.