TL;DR: Hergebruik uw DOM-elementen en verwijder de elementen die ver weg zijn van de viewport. Gebruik tijdelijke aanduidingen om rekening te houden met vertraagde gegevens. Hier is een demo en de code voor de oneindige scroller.
Oneindige scrollers verschijnen overal op internet. De artiestenlijst van Google Music is er één, de tijdlijn van Facebook is er één en de livefeed van Twitter is er ook één. Je scrolt naar beneden en voordat je de bodem bereikt, verschijnt nieuwe inhoud op magische wijze schijnbaar uit het niets. Het is een naadloze ervaring voor gebruikers en het is gemakkelijk om de aantrekkingskracht te zien.
De technische uitdaging achter een oneindige scroller is echter moeilijker dan het lijkt. De reeks problemen die u tegenkomt als u The Right Thing™ wilt doen, is enorm. Het begint met simpele dingen, zoals dat de links in de voettekst praktisch onbereikbaar worden omdat de inhoud de voettekst steeds wegduwt. Maar de problemen worden moeilijker. Hoe ga je om met een resize-gebeurtenis wanneer iemand zijn telefoon van staand naar liggend verandert, of hoe voorkom je dat je telefoon pijnlijk tot stilstand komt als de lijst te lang wordt?
Het juiste ding™
Wij vonden dat reden genoeg om met een referentie-implementatie te komen die een manier laat zien om al deze problemen op een herbruikbare manier aan te pakken met behoud van prestatienormen.
We gaan 3 technieken gebruiken om ons doel te bereiken: DOM-recycling, grafstenen en scroll-verankering.
Onze demo-case wordt een Hangouts-achtig chatvenster waarin we door de berichten kunnen scrollen. Het eerste dat we nodig hebben is een oneindige bron van chatberichten. Technisch gezien is geen van de oneindige scrollers die er zijn echt oneindig, maar met de hoeveelheid gegevens die beschikbaar is om in deze scrollers te worden gepompt, zouden ze dat net zo goed kunnen zijn. Voor de eenvoud zullen we gewoon een reeks chatberichten hardcoderen en willekeurig het bericht, de auteur en af en toe een afbeeldingsbijlage kiezen met een vleugje kunstmatige vertraging om zich een beetje meer als het echte netwerk te gedragen.
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 te gebruiken die buiten het scherm staan, in plaats van nieuwe te maken. Toegegeven, DOM-knooppunten zelf zijn goedkoop, maar ze zijn niet gratis, omdat elk van hen extra kosten met zich meebrengt op het gebied van geheugen, lay-out, stijl en verf. Low-end apparaten zullen merkbaar langzamer worden of zelfs volledig onbruikbaar als de website een te grote DOM heeft om te beheren. Houd er ook rekening mee dat elke heruitgave en hertoepassing van uw stijlen – een proces dat wordt geactiveerd wanneer een klasse wordt toegevoegd aan of verwijderd uit een knooppunt – duurder wordt met een grotere DOM. Het recyclen van uw DOM-nodes betekent dat we het totale aantal DOM-nodes aanzienlijk lager gaan houden, waardoor al deze processen sneller gaan.
De eerste hindernis is het scrollen zelf. Omdat we op elk moment slechts een kleine subset van alle beschikbare items in de DOM zullen hebben, moeten we een andere manier vinden om ervoor te zorgen dat de schuifbalk van de browser de hoeveelheid inhoud die er theoretisch aanwezig is, correct weergeeft. We zullen een Sentinel-element van 1px bij 1px gebruiken met een transformatie om het element dat de items bevat – de landingsbaan – te dwingen de gewenste hoogte te hebben. We zullen elk element in de landingsbaan naar een eigen laag promoten 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 zullen we een textuur op onze grafische kaart moeten opslaan met een hoogte van een paar honderdduizend pixels. Absoluut niet haalbaar op een mobiel apparaat.
Telkens wanneer we scrollen, controleren we of de viewport voldoende dicht bij het einde van de landingsbaan is gekomen. Als dat zo is, zullen we de start- en landingsbaan uitbreiden door het Sentinel-element te verplaatsen en de items die de viewport hebben verlaten naar de onderkant van de start- en landingsbaan te verplaatsen en deze te vullen met nieuwe inhoud.
Hetzelfde geldt voor scrollen in de andere richting. We zullen de start- en landingsbaan in onze implementatie echter nooit verkleinen, zodat de positie van de schuifbalk consistent blijft.
Grafstenen
Zoals we eerder vermeldden, proberen we onze gegevensbron zich te laten gedragen als iets in de echte wereld. Met netwerklatentie en zo. Dat betekent dat als onze gebruikers gebruik maken van flicky scrollen, ze gemakkelijk voorbij het laatste element kunnen scrollen waarvoor we gegevens hebben. Als dat gebeurt, plaatsen we een tombstone-item – een placeholder – dat wordt vervangen door het item met daadwerkelijke inhoud zodra de gegevens zijn binnengekomen. Grafstenen worden ook gerecycled en hebben een aparte poel voor herbruikbare DOM-elementen. We hebben dat nodig zodat we een mooie overgang kunnen maken van een grafsteen naar het item dat is gevuld met inhoud, wat anders erg schokkend zou zijn voor de gebruiker en ervoor zou kunnen zorgen dat ze uit het oog verliezen waar ze zich op concentreerden.
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 elke keer dat er gegevens binnenkomen en een tombstone boven de viewport wordt vervangen, waardoor de scrollpositie wordt verankerd aan een element in plaats van aan een pixelwaarde. Dit concept wordt scroll-verankering genoemd.
Scroll-verankering
Onze scroll-verankering wordt zowel geactiveerd wanneer grafstenen worden vervangen als wanneer het formaat van het venster wordt gewijzigd (wat ook gebeurt wanneer de apparaten worden omgedraaid!). We zullen moeten uitzoeken wat het meest 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.
Als het formaat van de viewport wordt gewijzigd en de landingsbaan verandert, kunnen we een situatie herstellen die visueel identiek aanvoelt voor de gebruiker. Winnen! Alleen betekent een aangepast venster dat elk item mogelijk van hoogte is veranderd. Hoe weten we dan hoe ver de verankerde inhoud naar beneden moet worden geplaatst? Wij niet! Om dit te weten te komen, moeten we elk element boven het verankerde item indelen en al hun hoogten bij elkaar optellen; dit kan een aanzienlijke pauze veroorzaken na het wijzigen van het formaat, en dat willen we niet. In plaats daarvan gaan we ervan uit dat elk item hierboven even groot is als een grafsteen en passen we onze scrollpositie dienovereenkomstig aan. Terwijl elementen de landingsbaan in worden gescrold, passen we onze scrollpositie aan, waardoor het lay-outwerk effectief wordt uitgesteld tot wanneer 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 doorgeven, wat ons ruim onder ons doel 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 bij transformaties. Op deze manier kunnen we doen alsof alle elementen verderop op de landingsbaan nog steeds ruimte innemen, terwijl er in werkelijkheid alleen maar lege ruimte is. Omdat we zelf aan de lay-out doen, kunnen we de posities waar elk item terechtkomt in de cache opslaan en kunnen we meteen het juiste element uit de cache laden als de gebruiker achteruit scrollt.
Idealiter zouden items slechts één keer opnieuw worden geverfd wanneer ze aan de DOM worden bevestigd en onaangedaan worden door toevoegingen of verwijderingen van andere items op de landingsbaan. Dat kan, maar alleen met moderne browsers.
Bloedstollende aanpassingen
Onlangs heeft Chrome ondersteuning toegevoegd voor CSS Containment , een functie waarmee ontwikkelaars aan de browser kunnen vertellen dat een element een grens is voor lay-out en schilderwerk. Omdat we hier zelf de lay-out doen, is het een uitstekende toepassing voor containment. Telkens wanneer we een element aan de landingsbaan toevoegen, weten we dat de andere items niet door de relay-out hoeven te worden beïnvloed. Elk item moet dus get contain: layout
zijn. We willen ook de rest van onze website niet beïnvloeden, dus de catwalk zelf zou deze stijlrichtlijn ook moeten krijgen.
Een ander ding dat we hebben overwogen is het gebruik IntersectionObservers
als een mechanisme om te detecteren wanneer de gebruiker ver genoeg heeft gescrolld zodat we elementen kunnen recyclen en nieuwe gegevens kunnen laden. IntersectionObservers zijn echter gespecificeerd met een hoge latentie (alsof je requestIdleCallback
gebruikt), dus we voelen ons misschien minder responsief met IntersectionObservers dan zonder. Zelfs onze huidige implementatie die de scroll
gebeurtenis gebruikt, heeft last van dit probleem, omdat scroll-gebeurtenissen op een “best effort”-basis worden verzonden. Uiteindelijk zou Houdini's Compositor Worklet de hifi-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 maar te letten op de elementen die daadwerkelijk op het scherm staan. Dit betekent dat wanneer u heel snel scrolt, u zoveel werk besteedt aan de lay-out en verf in Chrome dat het niet bij kan blijven. Uiteindelijk zie je niets anders dan de achtergrond. Het is niet het einde van de wereld, maar zeker iets om te verbeteren.
We hopen dat je ziet hoe uitdagend eenvoudige problemen kunnen zijn als je een geweldige gebruikerservaring wilt combineren met hoge prestatienormen. Nu Progressive Web Apps kernervaringen op mobiele telefoons worden, zal dit belangrijker worden en zullen webontwikkelaars moeten blijven investeren in het gebruik van patronen die prestatiebeperkingen respecteren.
Alle code is te vinden in onze repository . We hebben ons best gedaan om het herbruikbaar te houden, maar zullen het niet publiceren als een echte bibliotheek op npm of als een aparte opslagplaats. Het primaire gebruik is educatief.