Complexiteiten van een oneindige scroller

TL;DR: Hergebruik je DOM-elementen en verwijder de elementen die ver van de viewport staan. Gebruik tijdelijke aanduidingen om rekening te houden met vertraagde gegevens. Hier is een demo en de code voor de oneindige scroller.

Overal op internet duiken oneindige scrollers op. De artiestenlijst van Google Music is er één van, de tijdlijn van Facebook is er één en de livefeed van Twitter is er ook één. Je scrolt naar beneden en voordat je helemaal onderaan bent, verschijnt er op magische wijze nieuwe content, schijnbaar uit het niets. Het is een naadloze ervaring voor gebruikers en het is duidelijk waarom het zo aantrekkelijk is.

De technische uitdaging achter een oneindige scroller is echter groter dan het lijkt. De problemen die je tegenkomt als je The Right Thing™ wilt doen, zijn enorm. Het begint met simpele dingen, zoals links in de footer die praktisch onbereikbaar worden omdat de content de footer steeds verder wegduwt. Maar de problemen worden groter. Hoe ga je om met een resize-gebeurtenis wanneer iemand zijn telefoon van staand naar liggend draait, of hoe voorkom je dat je telefoon vastloopt wanneer de lijst te lang wordt?

Het juiste™

Wij vonden dat reden genoeg om met een referentie-implementatie te komen die liet zien hoe al deze problemen op een herbruikbare manier konden worden aangepakt, terwijl de prestatienormen behouden bleven.

Om ons doel te bereiken gaan we 3 technieken inzetten: DOM-recycling, grafstenen en rolverankering.

Onze democase is een Hangouts-achtig chatvenster waarin we door de berichten kunnen scrollen. Het eerste wat we nodig hebben, is een oneindige bron van chatberichten. Technisch gezien is geen van de oneindige scrollers echt oneindig, maar met de hoeveelheid data die beschikbaar is om in deze scrollers te pompen, zou dat net zo goed wel kunnen. Voor de eenvoud zullen we gewoon een set chatberichten hardcoderen en willekeurig het bericht, de auteur en af ​​en toe een afbeeldingsbijlage selecteren met een vleugje kunstmatige vertraging om zich een beetje meer als het echte netwerk te gedragen.

Schermafbeelding van de chat-app

DOM-recycling

DOM-recycling is een onderbenutte techniek om het aantal DOM-knooppunten laag te houden. Het algemene idee is om reeds gemaakte DOM-elementen die off-screen zijn te gebruiken in plaats van nieuwe te creëren. Toegegeven, DOM-knooppunten zelf zijn goedkoop, maar ze zijn niet gratis, omdat elk ervan extra kosten met zich meebrengt in geheugen, lay-out, stijl en tekenstijl. Low-end apparaten zullen merkbaar trager of zelfs volledig onbruikbaar worden als de website een te grote DOM heeft om te beheren. Houd er ook rekening mee dat elke relay-out en hertoepassing van je stijlen – een proces dat wordt geactiveerd wanneer een klasse wordt toegevoegd of verwijderd uit een knooppunt – duurder wordt met een grotere DOM. Door je DOM-knooppunten te recyclen, houden we het totale aantal DOM-knooppunten aanzienlijk lager, waardoor al deze processen sneller worden.

De eerste horde is het scrollen zelf. Omdat we op elk willekeurig moment slechts een kleine subset van alle beschikbare items in de DOM hebben, moeten we een andere manier vinden om de schuifbalk van de browser de theoretisch beschikbare hoeveelheid content correct te laten weergeven. We gebruiken een sentinel-element van 1 bij 1 px met een transformatie om het element met de items – de landingsbaan – de gewenste hoogte te geven. We promoveren elk element in de landingsbaan naar een eigen laag om ervoor te zorgen dat de laag van de landingsbaan zelf volledig leeg is. Geen achtergrondkleur, niets. Als de laag van de landingsbaan niet leeg is, komt deze niet in aanmerking voor de optimalisaties van de browser en moeten we een textuur met een hoogte van een paar honderdduizend pixels op onze grafische kaart opslaan. Absoluut niet bruikbaar op een mobiel apparaat.

Telkens wanneer we scrollen, controleren we of de viewport voldoende dicht bij het einde van de landingsbaan is gekomen. Zo ja, dan verlengen we de landingsbaan door het sentinel-element te verplaatsen en de items die de viewport hebben verlaten naar de onderkant van de landingsbaan te verplaatsen en ze te vullen met nieuwe content.

Startbaan Schildwacht Kijkvenster

Hetzelfde geldt voor scrollen in de andere richting. We zullen de landingsbaan echter nooit verkleinen in onze implementatie, zodat de positie van de schuifbalk consistent blijft.

Grafstenen

Zoals we eerder al aangaven, proberen we onze gegevensbron te laten functioneren als iets in de echte wereld. Inclusief netwerklatentie en alles wat daarbij hoort. Dat betekent dat als onze gebruikers gebruikmaken van flicky scrolling, ze gemakkelijk voorbij het laatste element kunnen scrollen waarvoor we gegevens hebben. In dat geval plaatsen we een tombstone-item – een tijdelijke aanduiding – dat wordt vervangen door het item met daadwerkelijke content zodra de gegevens binnen zijn. Tombstones worden ook gerecycled en hebben een aparte pool voor herbruikbare DOM-elementen. Dat hebben we nodig om een ​​mooie overgang te maken van een tombstone naar het item met content, wat anders erg storend zou zijn voor de gebruiker en ertoe zou kunnen leiden dat ze hun focus verliezen.

Wat een graf. Heel erg van steen. Wauw.

Een interessante uitdaging hierbij is dat echte items een grotere hoogte kunnen hebben dan het tombstone-item vanwege verschillende hoeveelheden tekst per item of een bijgevoegde afbeelding. Om dit op te lossen, passen we de huidige scrollpositie aan telkens wanneer er gegevens binnenkomen en een tombstone boven de viewport wordt geplaatst, waardoor de scrollpositie wordt verankerd aan een element in plaats van aan een pixelwaarde. Dit concept heet scroll-ankering.

Scroll-verankering

Onze scroll-ankering wordt zowel aangeroepen wanneer tombstones worden vervangen als wanneer het venster wordt vergroot of verkleind (wat ook gebeurt wanneer de apparaten worden gespiegeld!). We moeten uitzoeken wat het bovenste zichtbare element in de viewport is. Omdat dat element slechts gedeeltelijk zichtbaar kan zijn, slaan we ook de offset op vanaf de bovenkant van het element waar de viewport begint.

Scroll-ankerdiagram.

Als de viewport wordt aangepast en de landingsbaan verandert, kunnen we een situatie herstellen die visueel identiek aanvoelt voor de gebruiker. Win! Behalve dat een aangepast venster betekent dat elk item mogelijk zijn hoogte heeft gewijzigd, dus hoe weten we hoe ver de verankerde content naar beneden moet worden geplaatst? Nee! Om dit te achterhalen, zouden we elk element boven het verankerde item moeten positioneren en al hun hoogtes moeten optellen; dit zou een aanzienlijke pauze kunnen veroorzaken na een formaatwijziging, en dat willen we niet. In plaats daarvan gaan we ervan uit dat elk item daarboven dezelfde grootte heeft als een grafsteen en passen we onze scrollpositie dienovereenkomstig aan. Terwijl elementen naar de landingsbaan worden gescrold, passen we onze scrollpositie aan, waardoor het lay-outwerk in feite wordt uitgesteld tot het moment dat het daadwerkelijk nodig is.

Indeling

Ik heb een belangrijk detail overgeslagen: de lay-out. Elke recycling van een DOM-element zou normaal gesproken de hele landingsbaan opnieuw belichten, wat ons ruim onder onze doelstelling van 60 frames per seconde zou brengen. Om dit te voorkomen, nemen we de last van de lay-out op ons en gebruiken we absoluut gepositioneerde elementen met transformaties. Zo kunnen we doen alsof alle elementen verderop op de landingsbaan nog steeds ruimte innemen, terwijl er in werkelijkheid alleen lege ruimte is. Omdat we de lay-out zelf verzorgen, kunnen we de posities waar elk item terechtkomt cachen en kunnen we direct het juiste element uit de cache laden wanneer de gebruiker terugscrollt.

Idealiter zouden items slechts één keer opnieuw worden geschilderd wanneer ze aan de DOM worden gekoppeld en zouden ze niet beïnvloed worden door toevoegingen of verwijderingen van andere items op de catwalk. Dat is mogelijk, maar alleen met moderne browsers.

Geavanceerde aanpassingen

Onlangs heeft Chrome ondersteuning toegevoegd voor CSS Containment , een functie waarmee wij als ontwikkelaars de browser kunnen vertellen dat een element een grens vormt voor lay-out en tekenwerk. Omdat we hier zelf de lay-out verzorgen, is dit een uitstekende toepassing voor containment. Wanneer we een element aan de landingsbaan toevoegen, weten we dat de andere items niet beïnvloed hoeven te worden door de relayout. Elk item zou dus contain: layout moeten krijgen. We willen ook de rest van onze website niet beïnvloeden, dus de landingsbaan zelf zou deze stijlrichtlijn ook moeten krijgen.

We hebben ook overwogen om IntersectionObservers te gebruiken als mechanisme om te detecteren wanneer de gebruiker ver genoeg gescrolld heeft om elementen te recyclen en nieuwe data te laden. IntersectionObservers zijn echter gespecificeerd voor een hoge latentie (alsof requestIdleCallback wordt gebruikt), dus we zouden ons met IntersectionObservers mogelijk minder responsief kunnen voelen dan zonder. Zelfs onze huidige implementatie met de scroll -gebeurtenis heeft last van dit probleem, omdat scroll-gebeurtenissen op basis van "best effort" worden verzonden. Uiteindelijk zou Houdini's Compositor Worklet de high-fidelity oplossing voor dit probleem zijn.

Het is nog steeds niet perfect

Onze huidige implementatie van DOM-recycling is niet ideaal, omdat het alle elementen toevoegt die door de viewport gaan , in plaats van alleen de elementen die daadwerkelijk op het scherm staan. Dit betekent dat je, wanneer je echt snel scrollt, zoveel werk steekt in de lay-out en de tekenstijl van Chrome dat het systeem het niet bij kan benen. Je ziet uiteindelijk niets anders dan de achtergrond. Het is niet het einde van de wereld, maar zeker iets om te verbeteren.

We hopen dat u ziet hoe uitdagend eenvoudige problemen kunnen worden wanneer u een geweldige gebruikerservaring wilt combineren met hoge prestatienormen. Nu Progressive Web Apps de kern vormen van mobiele apps, wordt dit steeds belangrijker en zullen webontwikkelaars moeten blijven investeren in het gebruik van patronen die rekening houden met prestatiebeperkingen.

Alle code is te vinden in onze repository . We hebben ons best gedaan om de code herbruikbaar te houden, maar we zullen deze niet publiceren als een echte bibliotheek op npm of als een aparte repository. Het primaire gebruik is educatief.