Złożoność nieskończonego przewijania

TL;DR: wykorzystaj ponownie elementy DOM i usuń te, które są daleko od widocznego obszaru. Aby uwzględnić opóźnione dane, używaj obiektów zastępczych. Oto prezentacja oraz kod przewijania w nieskończoność.

W internecie pojawiają się nieskończone przewijanie. W Muzyce Google jest jedna lista wykonawców, jedna z nich to oś czasu na Facebooku, a na Twitterze również kanał na żywo. Przewijasz w dół, a zanim dotrzesz na sam dół, magicznie pojawiają się nowe treści. Wygoda użytkowników i dobre wrażenia.

Nieskończone przewijanie staje się jednak trudniejsze, niż się wydaje. Zakres problemów, jakie napotykasz, gdy chcesz podjąć właściwą decyzję, jest ogromny. Na początku linki w stopce stają się praktycznie nieosiągalne, ponieważ treść nieustannie odbija się od jej zawartości. Ale problemy stają się coraz trudniejsze. Co zrobić, gdy ktoś zmieni orientację telefonu z pionowej na poziomą lub co zrobić, aby telefon nie złamał się do bolesnego zatrzymania, gdy lista jest zbyt długa?

To, co jest właściweTM

Uznaliśmy, że to wystarcza, by opracować referencyjną implementację, która pozwoliłaby rozwiązać wszystkie te problemy w sposób wielokrotnego użytku przy zachowaniu standardów wydajności.

Aby osiągnąć ten cel, użyjemy 3 technik: recyklingu DOM, elementów tombstone i kotwicowania przewijania.

W naszym przypadku chodzi o okno czatu podobne do Hangouts, w którym można przewijać wiadomości. Pierwszą rzeczą, jakiej potrzebujemy, jest nieskończone źródło wiadomości na czacie. W zasadzie żaden z przewijanych treści nie jest naprawdę nieskończony, ale przy takiej ilości danych, jaką dysponujesz, żeby można było wprawić się w przewijanie, równie dobrze mogą być. Dla uproszczenia zakodujemy na stałe zestaw wiadomości czatu i wybierzemy losowo wiadomość, autora oraz sporadyczne załączniki graficzne z dodatkiem sztucznych opóźnień, aby urządzenie działało jak prawdziwe.

Zrzut ekranu z aplikacji Google Chat

Recykling DOM

Recykling DOM to niedostatecznie wykorzystywana metoda utrzymywania niskiej liczby węzłów DOM. Ogólnie zamiast tworzyć nowe elementy, należy używać utworzonych już elementów DOM, które znajdują się poza ekranem. Węzły DOM są tanie, ale nie są bezpłatne, ponieważ każdy z nich wiąże się z dodatkowymi kosztami pamięci, układu, stylu i malowania. Mniej zaawansowane urządzenia działają znacznie wolniej, jeśli witryna zawiera zbyt duży DOM, aby można było nim zarządzać. Pamiętaj też, że każde przekazanie i ponowne zastosowanie stylów, czyli proces uruchamiany za każdym razem, gdy klasa jest dodawana do węzła lub z niego usunięta, staje się droższa przy większym DOM. Recykling węzłów DOM oznacza, że łączna liczba węzłów DOM znacznie spadnie, co przyspieszy wszystkie te procesy.

Pierwsza przeszkoda to samo przewijanie. Ponieważ w DOM dostępny jest tylko niewielki podzbiór wszystkich elementów dostępnych w DOM, musimy znaleźć inny sposób na to, aby pasek przewijania w przeglądarce odpowiednio odzwierciedlał ilość treści, która jest tam w teorii. Wykorzystamy element śledzący o wymiarach 1 x 1 piksel z przekształceniem, aby wymusić odpowiednią wysokość elementu, który zawiera te elementy – pasa startowego. Każdy element na pasie startowym zostanie przesunięty na własną warstwę, tak aby warstwa pasa startowego była całkowicie pusta. Nic. Jeśli warstwa pasa startowego nie jest pusta, nie można jej wykorzystać do optymalizacji przeglądarki i trzeba będzie zapisać na karcie graficznej teksturę o wysokości kilkuset tysięcy pikseli. Z pewnością nie działa na urządzeniach mobilnych.

Za każdym razem, gdy przewijamy stronę, sprawdzamy, czy widoczny obszar wystarczająco zbliży się do końca pasa startowego. Jeśli tak, wydłużymy pas startowy, przesuwając element śledzący. Elementy, które opuściły widoczny obszar, na dół pasa startowego, i wypełniamy je nowymi treściami.

Uruchomienie Sentinel } }

To samo dotyczy przewijania w drugą stronę. Nigdy jednak nie zmniejszymy pasa startowego w ramach naszej implementacji, aby pozycja paska przewijania pozostała spójna.

Elementy tombstone

Jak już wspomnieliśmy, staramy się, aby nasze źródło danych działało jak rzeczywiste źródło danych. Opóźnienia sieciowe i wszystko inne. To oznacza, że jeśli użytkownicy skorzystają z nieprawidłowego przewijania strony, mogą bez problemu przewinąć stronę z ostatnim elementem, którego dotyczą dane. W takim przypadku umieszczamy element nagrobkowy (zastępczą) element, który po otrzymaniu danych zostanie zastąpiony rzeczywistym treścią. Elementy nagrobkowe są też poddawane recyklingowi i mają oddzielną pulę dla elementów DOM do wielokrotnego użytku. Jest to konieczne, abyśmy mogli płynnie przejść z grobowca do elementu wypełnionego treściami, które w przeciwnym razie byłyby bardzo drażliwe dla użytkownika i mogłyby sprawić, że zapominał o tym, na czym się skupiał.

Taki grób. Bardzo kamienny. Niesamowite.

Ciekawe wyzwanie jest takie, że prawdziwe przedmioty mogą mieć większą wysokość niż element nagrobkowy ze względu na różną ilość tekstu na element lub na załączony obraz. Aby rozwiązać ten problem, dostosujemy bieżącą pozycję przewijania za każdym razem, gdy przychodzą dane i zastępowany jest nagrobek nad widocznym obszarem, zakotwiczając pozycję przewijania do elementu, a nie wartości w pikselu. Jest to tzw. zakotwiczenie przewijania.

Zakotwiczenie przewijania

Nasze zakotwiczenie przewijania jest wywoływane zarówno przy zastępowaniu elementów nagrobkowych, jak i przy zmianie rozmiaru okna (co również ma miejsce podczas odwracania urządzeń). Musimy ustalić, jaki jest najwyższy widoczny element w widocznym obszarze. Ponieważ ten element może być tylko częściowo widoczny, zachowamy również przesunięcie od góry elementu w miejscu, w którym zaczyna się widoczny obszar.

Przewiń schemat zakotwiczenia.

Jeśli rozmiar widocznego obszaru zostanie zmieniony, a pas startowy ulegnie zmianie, możemy przywrócić sytuację, która wydaje się identyczna wizualnie z punktem widzenia użytkownika. Wygrana! Poza tym, że okno ze zmianą rozmiaru może się zmienić wysokość każdego elementu, jak daleko mamy umieścić zakotwiczoną treść? Nie! Aby to stwierdzić, musielibyśmy umieścić każdy element nad zakotwiczonym elementem i dodać wszystkie ich wysokości. Może to spowodować spore przerwy po zmianie rozmiaru, choć nie chcemy tego robić. Zamiast tego przyjmujemy, że każdy element powyżej ma taki sam rozmiar jak nagrobek i odpowiednio dostosowujemy pozycję przewijania. Gdy elementy są przeciągnięte na pas startowy, dostosowujemy pozycję przewijania, dzięki czemu układ strony jest w czasie rzeczywistym opóźniony.

Układ

Pominąłem ważny szczegół: układ. W przypadku każdego recyklingu elementu DOM normalnie obrysowany zostałby cały pas startowy, co znacznie spadłoby do docelowej liczby klatek na sekundę (60 klatek na sekundę). Aby tego uniknąć, przyjmujemy na siebie ciężar układu i używamy precyzyjnie rozmieszczonych elementów z transformami. Dzięki temu możemy udawać, że wszystkie elementy dalej na pasie startowym nadal zajmują przestrzeń, podczas gdy w rzeczywistości jest tam tylko pusta przestrzeń. Ponieważ opracowujemy układ, możemy zapisać w pamięci podręcznej pozycje, w których kończy się każdy element, i natychmiast wczytywać odpowiedni element z pamięci podręcznej, gdy użytkownik przewinie do tyłu.

Najlepiej, gdyby elementy zostały ponownie pomalowane tylko raz, gdy są przyłączone do DOM, i nie przeszkadzają w ich wyświetlaniu przez dodanie lub usunięcie innych elementów na pasie startowym. Jest to możliwe, ale tylko w nowoczesnych przeglądarkach.

Najnowsze poprawki

Ostatnio w Chrome dodaliśmy obsługę Pojemności CSS – funkcję, która pozwala deweloperom poinformować przeglądarkę, że element jest granicą układu i obrazu. Ponieważ sami zajmujemy się układem, jest to dobra aplikacja do ograniczania dostępu. Za każdym razem, gdy dodajemy element do pasa startowego, wiemy, że inne elementy nie muszą mieć wpływu na pozostałe elementy. Każdy element powinien więc mieć contain: layout. Nie chcemy też wpływać na pozostałą część naszej witryny, więc sam pas startowy powinien być zgodny z tą dyrektywą.

Kolejną rzeczą, jaką rozważaliśmy, jest użycie IntersectionObservers jako mechanizmu wykrywającego, czy użytkownik przewinął na tyle daleko, że możemy rozpocząć recykling elementów i wczytać nowe dane. Jednak serwer IntersectionObservers charakteryzuje się długim czasem oczekiwania (tak jak w przypadku użycia requestIdleCallback), więc jego responsywność może być słabsza niż bez niego. Problem ten występuje nawet w przypadku naszej obecnej implementacji korzystającej ze zdarzenia scroll, ponieważ zdarzenia przewijania są wysyłane w najlepszej możliwej jakości. W ostatecznym rozrachunku rozwiązaniem tego problemu będzie Worklet kompozytora Houdiniego.

Nadal nie jest idealna

Obecna implementacja recyklingu DOM nie jest idealna, ponieważ uwzględnia wszystkie elementy, które przechodzą przez widoczny obszar, zamiast zajmować się tylko tymi, które znajdują się na ekranie. Oznacza to, że gdy przewijasz bardzo szybko, musisz poświęcać tyle czasu na układ i malowanie Chrome, że nie jest w stanie nadążyć. Zobaczysz tylko tło. To nie koniec świata, ale z pewnością będzie tu coś do poprawy.

Mamy nadzieję, że dostrzeżesz, jak trudne mogą być proste problemy, gdy chcesz połączyć wygodę użytkowników z wysokimi standardami wydajności. W miarę jak progresywne aplikacje internetowe stają się podstawowymi interfejsami na telefonach komórkowych, staje się to ważniejsze, a deweloperzy stron internetowych będą musieli dalej inwestować w stosowanie wzorców uwzględniających ograniczenia wydajności.

Cały kod znajdziesz w naszym repozytorium. Dołożyliśmy wszelkich starań, aby można było jej ponownie używać, ale nie opublikujemy jej jako rzeczywistej biblioteki w npm ani jako osobnego repozytorium. Mają głównie charakter edukacyjny.