W skrócie: ponownie używaj elementów DOM i usuwaj te, które są daleko od obszaru widocznego. Używaj symboli zastępczych, aby uwzględniać opóźnione dane. Oto demo i kod nieskończonego przewijania.
Przewijanie nieskończone pojawia się w całym internecie. Lista wykonawców w Google Music, oś czasu na Facebooku i kanał na żywo na Twitterze to przykłady takich list. Przewijasz stronę w dół i zanim dotrzesz do końca, pojawiają się nowe treści, jakby znikąd. Jest to wygodne rozwiązanie dla użytkowników, dlatego łatwo zrozumieć, dlaczego jest tak popularne.
Wyzwanie techniczne związane z nieskończonym przewijaniem jest jednak trudniejsze, niż się wydaje. Zakres problemów, z którymi możesz się spotkać, gdy chcesz postąpić właściwie, jest ogromny. Zaczyna się od prostych rzeczy, takich jak linki w stopce, które stają się praktycznie niedostępne, ponieważ treść stale odsuwa stopkę. Ale problemy stają się trudniejsze. Jak radzisz sobie ze zmianą rozmiaru, gdy ktoś obraca telefon z trybu pionowego do poziomego, lub jak zapobiec nagłemu zatrzymaniu telefonu, gdy lista staje się zbyt długa?
The right thing™
Uznaliśmy, że to wystarczający powód, aby opracować implementację referencyjną, która pokazuje, jak rozwiązać wszystkie te problemy w sposób wielokrotnego użytku przy zachowaniu standardów wydajności.
Aby osiągnąć ten cel, zastosujemy 3 techniki: recykling DOM, znaczniki i zakotwiczanie przewijania.
Nasz przykładowy przypadek będzie wyglądał jak okno czatu podobne do Hangouts, w którym możemy przewijać wiadomości. Pierwsze, czego potrzebujemy, to nieskończone źródło wiadomości na czacie. Żaden z obecnych na rynku elementów przewijanych w nieskończoność nie jest prawdziwie nieskończony, ale ilość danych, które można w nich umieścić, sprawia, że można je za takie uznać. Dla uproszczenia zakodujemy na stałe zestaw wiadomości na czacie i będziemy losowo wybierać wiadomości, autorów i załączniki w postaci obrazów, dodając od czasu do czasu sztuczne opóźnienie, aby zachowywać się bardziej jak prawdziwa sieć.

Recykling DOM
Recykling DOM to niedostatecznie wykorzystywana technika, która pozwala utrzymać niską liczbę węzłów DOM. Ogólna idea polega na używaniu już utworzonych elementów DOM, które znajdują się poza ekranem, zamiast tworzyć nowe. Węzły DOM są co prawda tanie, ale nie są bezpłatne, ponieważ każdy z nich zwiększa koszt pamięci, układu, stylu i malowania. Na urządzeniach z niższej półki witryny z zbyt dużym DOM mogą działać znacznie wolniej, a nawet stać się bezużyteczne. Pamiętaj też, że każde ponowne rozmieszczenie i ponowne zastosowanie stylów – proces wywoływany za każdym razem, gdy do węzła dodawana lub z niego usuwana jest klasa – staje się bardziej kosztowne wraz ze wzrostem rozmiaru DOM. Recykling węzłów DOM oznacza, że utrzymamy znacznie niższą łączną liczbę węzłów DOM, co przyspieszy wszystkie te procesy.
Pierwszą przeszkodą jest samo przewijanie. W dowolnym momencie w DOM będziemy mieć tylko niewielki podzbiór wszystkich dostępnych elementów, dlatego musimy znaleźć inny sposób, aby pasek przewijania przeglądarki prawidłowo odzwierciedlał ilość treści, która teoretycznie się tam znajduje. Użyjemy elementu strażniczego o wymiarach 1 × 1 piksela z transformacją, aby wymusić na elemencie zawierającym produkty – pasie startowym – pożądaną wysokość. Każdy element pasa startowego przeniesiemy do osobnej warstwy, aby warstwa samego pasa startowego była całkowicie pusta. Brak koloru tła, nic. Jeśli warstwa pasa startowego nie jest pusta, nie kwalifikuje się do optymalizacji przeglądarki i musimy przechowywać na karcie graficznej teksturę o wysokości kilkuset tysięcy pikseli. Zdecydowanie nie nadaje się do używania na urządzeniu mobilnym.
Podczas przewijania sprawdzamy, czy obszar widoku wystarczająco zbliżył się do końca pasa. W takim przypadku wydłużymy pas startowy, przesuwając element wartowniczy i przenosząc elementy, które opuściły obszar widoczny, na dół pasa startowego, a następnie wypełnimy je nowymi treściami.
To samo dotyczy przewijania w drugą stronę. Nigdy jednak nie zmniejszymy zakresu w naszej implementacji, aby pozycja paska przewijania pozostała spójna.
Elementy tombstone
Jak wspomnieliśmy wcześniej, staramy się, aby nasze źródło danych zachowywało się jak coś w świecie rzeczywistym. Z opóźnieniem sieciowym i wszystkim. Oznacza to, że jeśli użytkownicy korzystają z szybkiego przewijania, mogą łatwo przewinąć ostatni element, dla którego mamy dane. W takim przypadku umieścimy element zastępczy, który zostanie zastąpiony elementem z rzeczywistą treścią, gdy dane dotrą. Nagrobki są również poddawane recyklingowi i mają oddzielną pulę elementów DOM, które można ponownie wykorzystać. Jest to potrzebne, aby przejście od informacji o produkcie do elementu wypełnionego treścią było płynne. W przeciwnym razie użytkownik mógłby się zdezorientować i stracić z oczu to, na czym się skupiał.

Ciekawym wyzwaniem jest to, że rzeczywiste elementy mogą być wyższe niż elementy zastępcze ze względu na różną ilość tekstu w poszczególnych elementach lub dołączony obraz. Aby rozwiązać ten problem, będziemy dostosowywać bieżącą pozycję przewijania za każdym razem, gdy pojawią się dane, a nad obszarem widoku zostanie zastąpiony nagrobek. Będziemy przytwierdzać pozycję przewijania do elementu, a nie do wartości piksela. Koncepcja ta jest nazywana zakotwiczeniem przewijania.
Zakotwiczenie przewijania
Nasze zakotwiczenie przewijania będzie wywoływane zarówno wtedy, gdy zastępowane są znaczniki, jak i wtedy, gdy zmieniany jest rozmiar okna (co dzieje się również wtedy, gdy urządzenie jest obracane). Musimy ustalić, który element jest najbardziej widoczny w widocznym obszarze. Ponieważ ten element może być widoczny tylko częściowo, zapiszemy też przesunięcie od góry elementu, w którym zaczyna się widoczny obszar.

Jeśli rozmiar obszaru wyświetlania zostanie zmieniony, a obszar startowy ulegnie zmianom, możemy przywrócić sytuację, która będzie wizualnie identyczna dla użytkownika. Wygrana! Z wyjątkiem zmiany rozmiaru okna, która oznacza, że wysokość każdego elementu mogła się zmienić. Jak więc określić, jak daleko w dół należy umieścić zakotwiczoną treść? Nie! Aby to sprawdzić, musielibyśmy rozmieścić wszystkie elementy nad zakotwiczonym elementem i zsumować ich wysokości. Mogłoby to spowodować znaczną przerwę po zmianie rozmiaru, a tego nie chcemy. Zamiast tego zakładamy, że każdy z tych elementów ma taki sam rozmiar jak nagrobek, i odpowiednio dostosowujemy pozycję przewijania. Gdy elementy są przewijane na pas startowy, dostosowujemy pozycję przewijania, skutecznie odkładając pracę związaną z układem na moment, w którym jest ona rzeczywiście potrzebna.
Układ
Pominąłem ważny szczegół: układ. Każde ponowne użycie elementu DOM zwykle powoduje ponowne rozmieszczenie całej ścieżki, co znacznie obniża liczbę klatek na sekundę poniżej docelowych 60. Aby tego uniknąć, bierzemy na siebie ciężar układu i używamy elementów pozycjonowanych bezwzględnie z przekształceniami. W ten sposób możemy udawać, że wszystkie elementy znajdujące się dalej na pasie startowym nadal zajmują miejsce, choć w rzeczywistości jest tam tylko pusta przestrzeń. Ponieważ sami tworzymy układ, możemy zapisywać w pamięci podręcznej pozycje, na których znajdują się poszczególne elementy, i natychmiast wczytywać z niej odpowiedni element, gdy użytkownik przewija stronę do tyłu.
W idealnej sytuacji elementy byłyby odmalowywane tylko raz, gdy zostaną dołączone do DOM, i nie miałyby na nie wpływu dodawanie ani usuwanie innych elementów na pasie. Jest to możliwe, ale tylko w przypadku nowoczesnych przeglądarek.
Najnowsze ulepszenia
Niedawno Chrome dodał obsługę funkcji CSS Containment, która pozwala deweloperom informować przeglądarkę, że dany element jest granicą dla układu i renderowania. Ponieważ sami tworzymy tutaj układ, jest to doskonałe zastosowanie dla ograniczenia. Gdy dodajemy element do pasa startowego, wiemy, że nie musimy zmieniać układu innych elementów. Każdy produkt powinien otrzymać contain: layout
. Nie chcemy też wpływać na resztę naszej witryny, więc sam pas startowy powinien również otrzymać tę dyrektywę stylu.
Rozważaliśmy też użycie
IntersectionObservers
jako mechanizmu wykrywania, kiedy użytkownik przewinie stronę na tyle daleko, abyśmy mogli zacząć ponownie wykorzystywać elementy i wczytywać nowe dane. Jednak interfejsy IntersectionObserver mają z założenia duże opóźnienie (jak w przypadku używania requestIdleCallback
), więc w rzeczywistości możemy odczuwać mniejszą responsywność w przypadku interfejsów IntersectionObserver niż bez nich. Nawet nasza obecna implementacja korzystająca ze zdarzenia scroll
ma ten problem, ponieważ zdarzenia przewijania są wysyłane na zasadzie „najlepszych starań”. Ostatecznie Houdini’s Compositor Worklet
będzie rozwiązaniem tego problemu o wysokiej wierności.
Nadal nie jest idealny
Obecna implementacja recyklingu DOM nie jest idealna, ponieważ dodaje wszystkie elementy, które przechodzą przez obszar widoczny, zamiast uwzględniać tylko te, które są na ekranie. Oznacza to, że gdy przewijasz bardzo szybko, Chrome musi wykonać tak dużo pracy związanej z układem i rysowaniem, że nie nadąża. W efekcie zobaczysz tylko tło. To nie koniec świata, ale zdecydowanie jest to coś, co można poprawić.
Mamy nadzieję, że widzisz, jak proste problemy mogą się skomplikować, gdy chcesz połączyć wygodę użytkowników z wysokimi standardami wydajności. Progresywne aplikacje internetowe stają się podstawowym sposobem korzystania z telefonów komórkowych, dlatego będzie to coraz ważniejsze, a programiści internetowi będą musieli nadal inwestować w wzorce, które uwzględniają ograniczenia wydajności.
Cały kod znajdziesz w naszym repozytorium. Staraliśmy się, aby był on wielokrotnego użytku, ale nie opublikujemy go jako biblioteki w npm ani jako osobnego repozytorium. Głównym celem jest edukacja.