Complessità di uno scorrimento continuo

TL;DR: Riutilizza gli elementi DOM e rimuovi quelli distanti dall'area visibile. Utilizza i segnaposto per tenere conto dei dati in ritardo. Ecco una demo e il codice per lo scorrimento continuo.

Su Internet compaiono infiniti scorrimenti. L'elenco degli artisti di Google Music è uno, la sequenza temporale di Facebook e il feed live di Twitter lo sono. Scorrendo verso il basso, prima di arrivare in fondo, appaiono magicamente nuovi contenuti dal nulla. Offre agli utenti un'esperienza fluida e facilmente vedere il richiamo.

La sfida tecnica dietro uno scroller infinito, tuttavia, è più difficile di quanto sembri. La gamma di problemi che puoi riscontrare quando vuoi fare The Right ThingTM è enorme. Inizia con cose semplici come i link nel piè di pagina che diventano praticamente irraggiungibili perché i contenuti continuano a spingere via il piè di pagina. Ma i problemi diventano più difficili. Come si gestisce un evento di ridimensionamento quando un utente passa dall'orientamento verticale a quello orizzontale o come si fa a evitare che il telefono si interrompa quando l'elenco diventa troppo lungo?

La cosa giustaTM

Abbiamo pensato che fosse un motivo sufficiente per creare un'implementazione di riferimento che mostra un modo per affrontare tutti questi problemi in un modo riutilizzabile, mantenendo al contempo gli standard di prestazioni.

Per raggiungere il nostro obiettivo, utilizzeremo 3 tecniche: riciclo del DOM, lapidi e ancoraggio a scorrimento.

Il nostro caso dimostrativo sarà una finestra di chat in stile Hangouts dove potremo scorrere i messaggi. La prima cosa di cui abbiamo bisogno è una fonte infinita di messaggi di chat. Tecnicamente, nessuno degli scorrimenti infiniti è davvero infinito, ma con la quantità di dati che è disponibile per essere pompati in questi tipi di scorrimento potrebbero esserlo. Per semplicità, implementeremo un set di messaggi di chat hardcoded e sceglieremo a caso messaggi, autori e allegati di immagini occasionali, con un pizzico di ritardo artificiale per comportarsi in modo un po' più simile alla rete reale.

Screenshot dell'app di chat

Riciclo DOM

Il riciclo del DOM è una tecnica sottoutilizzata per mantenere basso il numero di nodi DOM. L'idea generale è utilizzare elementi DOM già creati che non sono sullo schermo anziché crearne di nuovi. Certo, i nodi DOM di per sé sono economici, ma non sono senza costi, in quanto ciascuno di questi comporta costi aggiuntivi in termini di memoria, layout, stile e colorazione. I dispositivi di fascia bassa saranno notevolmente più lenti se non sono completamente inutilizzabili se il sito web ha un DOM troppo grande da gestire. Inoltre, tieni presente che ogni relayout e riapplicazione degli stili (un processo che viene attivato ogni volta che una classe viene aggiunta o rimossa da un nodo) diventa più costoso con un DOM più grande. Riciclare i nodi DOM significa che manterremo notevolmente inferiore il numero totale di nodi DOM, rendendo tutti questi processi più veloci.

Il primo ostacolo è lo scorrimento stesso. Poiché avremo solo un piccolo sottoinsieme di tutti gli elementi disponibili nel DOM in un determinato momento, dobbiamo trovare un altro modo per fare in modo che la barra di scorrimento del browser rifletta correttamente la quantità di contenuti teoricamente presenti. Utilizzeremo un elemento sentinel di 1 x 1 pixel con una trasformazione per forzare l'elemento che contiene gli oggetti, ovvero la pista, ad avere l'altezza desiderata. Promuovere ogni elemento in passerella a un livello specifico per fare in modo che il livello della passerella sia completamente vuoto. Nessun colore di sfondo, niente. Se il livello della pista non è vuoto, non è idoneo per le ottimizzazioni del browser e dovremo archiviare una texture sulla nostra scheda grafica che ha un'altezza di duecentomila pixel. Assolutamente non utilizzabile su un dispositivo mobile.

Ogni volta che scorriamo, controlliamo se l'area visibile si è avvicinata sufficientemente alla fine della pista. In tal caso, estenderemo la pista spostando l'elemento sentinel, spostando gli elementi che hanno lasciato l'area visibile in fondo all'area visibile e popolandoli con nuovi contenuti.

Runway Sentinel

Lo stesso vale per lo scorrimento nell'altra direzione. Tuttavia, non restringeremo mai la pista nella nostra implementazione, in modo che la posizione della barra di scorrimento rimanga costante.

Lapidi

Come accennato in precedenza, cerchiamo di fare in modo che la nostra origine dati si comporti come qualcosa nel mondo reale. Con latenza di rete e tutto il resto. Ciò significa che se i nostri utenti utilizzano lo scorrimento fluido, possono facilmente scorrere oltre l'ultimo elemento per il quale disponiamo di dati. In questo caso, posizioneremo un articolo di lapide, ovvero un segnaposto, che verrà sostituito dall'articolo con i contenuti effettivi una volta ricevuti i dati. Anche le lapidi vengono riciclate e dispongono di un pool separato per gli elementi DOM riutilizzabili. Ne abbiamo bisogno per poter effettuare una transizione piacevole da una lapide all'elemento ricco di contenuti, che altrimenti sarebbero molto sgradevoli per l'utente e potrebbe fargli perdere di vista ciò su cui si stava concentrando.

Questa tomba. Molto in pietra. Wow.

Una sfida interessante in questo caso è che gli oggetti reali possono avere un'altezza maggiore rispetto all'elemento tombale a causa di quantità diverse di testo per elemento o di un'immagine allegata. Per risolvere il problema, regoleremo la posizione di scorrimento corrente ogni volta che vengono inseriti dati e una lapide viene sostituita sopra l'area visibile, ancorando la posizione di scorrimento a un elemento anziché a un valore di pixel. Questo concetto è chiamato ancoraggio di scorrimento.

Ancoraggio di scorrimento

L'ancoraggio di scorrimento viene attivato sia quando le lapidi vengono sostituite sia quando la finestra viene ridimensionata (il che accade anche quando i dispositivi vengono capovolti). Dovremo capire qual è l'elemento più in alto nell'area visibile. Poiché questo elemento potrebbe essere visibile solo parzialmente, memorizzeremo anche l'offset dalla parte superiore dell'elemento nel punto in cui inizia l'area visibile.

Scorri il diagramma di ancoraggio.

Se l'area visibile viene ridimensionata e la pista cambia, possiamo ripristinare una situazione visivamente identica all'utente. Vittoria! Tranne che la finestra ridimensionata significa che ogni elemento ha potenzialmente modificato la sua altezza. Quindi, come faccio a sapere fino a che punto devono essere posizionati i contenuti ancorati? Per noi no. Per scoprire dovremmo impostare il layout di ogni elemento sopra l'elemento ancorato e sommarne tutte le altezze; questo potrebbe causare una pausa significativa dopo un ridimensionamento, cosa che non è voluto. Ricorriamo invece al presupposto che tutti gli elementi sopra riportati abbiano le stesse dimensioni di una lapide e modifichiamo di conseguenza la posizione di scorrimento. Quando gli elementi vengono fatti scorrere nella passerella, regoliamo la posizione di scorrimento, rinviando di fatto il lavoro di layout a quando è effettivamente necessario.

Layout

Ho saltato un dettaglio importante: il layout. Ogni riciclaggio di un elemento DOM normalmente ridisegna l'intera passerella portandoci ben al di sotto del nostro obiettivo di 60 frame al secondo. Per evitarlo, ci assumiamo l'onere del layout e utilizziamo elementi con posizione assoluta e trasformazioni. In questo modo possiamo far finta che tutti gli elementi più in alto sulla passerella stiano ancora occupando spazio, mentre in realtà c'è solo uno spazio vuoto. Poiché eseguiamo il layout, possiamo memorizzare nella cache le posizioni di fine di ogni elemento e caricare immediatamente l'elemento corretto dalla cache quando l'utente scorre indietro.

Idealmente, gli oggetti verrebbero ridipinti una volta sola quando attaccati al DOM e non rischiano di essere alterati dall'aggiunta o dalla rimozione di altri oggetti in passerella. Questo è possibile, ma solo con i browser moderni.

Piccoli ritocchi

Recentemente, Chrome ha aggiunto il supporto del contenimento CSS, una funzionalità che consente agli sviluppatori di indicare al browser che un elemento è un limite per le operazioni di layout e colorazione. Poiché stiamo realizzando il layout qui, è un'applicazione fondamentale per il contenimento. Ogni volta che aggiungiamo un elemento alla passerella, sappiamo che gli altri elementi non devono essere interessati dal relayout. Quindi, ogni articolo dovrebbe essere contain: layout. Inoltre, non vogliamo che influisca sul resto del nostro sito web, quindi anche la passerella dovrebbe avere questa istruzione di stile.

Un altro aspetto che abbiamo preso in considerazione è l'utilizzo di IntersectionObservers come meccanismo per rilevare quando l'utente ha fatto scorrere la pagina fino al punto in cui possiamo iniziare a riciclare gli elementi e caricare nuovi dati. Tuttavia, gli IntersectionCommentrs hanno specificato una latenza elevata (come se si utilizzasse requestIdleCallback), di conseguenza potremmo sentirci meno reattivi con IntersectionObservationrs rispetto a senza. Anche la nostra attuale implementazione con l'evento scroll presenta questo problema, poiché gli eventi di scorrimento vengono inviati secondo il criterio del "best effort". Alla fine, Houdini's Compositor Worklet sarebbe la soluzione ad alta fedeltà a questo problema.

Non è ancora perfetta

L'attuale implementazione del riciclo del DOM non è l'ideale, in quanto aggiunge tutti gli elementi che trascorrono attraverso l'area visibile, anziché preoccuparsi solo di quelli che sono effettivamente sullo schermo. Ciò significa che quando scorri davvero velocemente, lavori così tanto per il layout e la pittura su Chrome che non riesci a tenere il passo. Vedrai solo lo sfondo. Non è la fine del mondo, ma è sicuramente da migliorare.

Ci auguriamo che tu capisca quanto possano essere complicati i problemi semplici quando vuoi combinare un'ottima esperienza utente con standard di prestazioni elevati. Con le applicazioni web progressive che stanno diventando un'esperienza fondamentale sui telefoni cellulari, questo aspetto diventerà sempre più importante e gli sviluppatori web dovranno continuare a investire nell'utilizzo di pattern che rispettino i limiti di prestazioni.

Tutto il codice è disponibile nel nostro repository. Abbiamo fatto del nostro meglio per mantenerlo riutilizzabile, ma non lo pubblicheremo come una libreria effettiva su npm o come repository separato. L'utilizzo principale è didattico.