Complessità di uno scorrimento continuo

TL;DR: riutilizza gli elementi DOM e rimuovi quelli lontani dal viewport. Utilizza i segnaposto per tenere conto dei dati in ritardo. Ecco una demo e il codice per lo scorrimento infinito.

Gli annunci con scorrimento continuo vengono visualizzati su internet. L'elenco di artisti di Google Music è uno, la cronologia di Facebook è un'altra e lo stesso vale per il feed in tempo reale di Twitter. Scorri verso il basso e, prima di raggiungere la fine, nuovi contenuti compaiono magicamente apparentemente dal nulla. È un'esperienza fluida per gli utenti ed è facile capire il suo appeal.

Tuttavia, la sfida tecnica alla base di uno scorrimento infinito è più difficile di quanto sembri. La gamma di problemi che si verificano quando vuoi fare la cosa giusta è vasta. Inizia con cose semplici, come i link nel piè di pagina che diventano praticamente irraggiungibili perché i contenuti continuano a spingere il piè di pagina verso l'alto. Ma i problemi diventano più difficili. Come gestisci un evento di ridimensionamento quando qualcuno gira lo smartphone da verticale a orizzontale o come fai a evitare che lo smartphone si blocchi quando l'elenco diventa troppo lungo?

The right thing™

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

Per raggiungere il nostro obiettivo, utilizzeremo tre tecniche: riciclo del DOM, tombstones e ancoraggio allo scorrimento.

La nostra richiesta di dimostrazione sarà una finestra della chat simile a Hangouts in cui potremo scorrere i messaggi. La prima cosa di cui abbiamo bisogno è un'infinita fonte di messaggi di chat. Tecnicamente, nessuno degli scorrimenti infiniti disponibili è veramente infinito, ma con la quantità di dati disponibili per essere inseriti in questi scorrimenti, potrebbero esserlo. Per semplicità, imposteremo un insieme di messaggi di chat e sceglieremo messaggio, autore e occasionali allegati di immagini in modo casuale con un po' di ritardo artificiale per comportarci un po' di più come la rete reale.

Screenshot dell'app di chat

Riciclo del 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 visibili sullo schermo anziché crearne di nuovi. È vero che i nodi DOM sono economici, ma non sono senza costi, poiché ognuno di essi aggiunge un costo aggiuntivo in termini di memoria, layout, stile e rendering. I dispositivi di fascia bassa diventeranno notevolmente più lenti, se non completamente inutilizzabili, se il sito web ha un DOM troppo grande da gestire. Tieni inoltre presente che ogni nuovo layout e ogni nuova applicazione 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. Il riciclo dei nodi DOM significa che manterremo notevolmente più basso il numero totale di nodi DOM, rendendo più veloci tutte queste procedure.

Il primo ostacolo è lo scorrimento stesso. Poiché in un determinato momento avremo solo un piccolo sottoinsieme di tutti gli elementi disponibili nel DOM, 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 sentinella di 1 x 1 px con una trasformazione per forzare l'elemento che contiene gli elementi, ovvero la passerella, ad avere l'altezza desiderata. Promuoveremo ogni elemento della passerella in un proprio livello per assicurarci che il livello della passerella stessa sia completamente vuoto. Nessun colore di sfondo, niente. Se il livello della pista non è vuoto, non è idoneo per le ottimizzazioni del browser e dovremo memorizzare una texture sulla scheda grafica con un'altezza di un paio di centinaia di migliaia di pixel. Sicuramente non fattibile su un dispositivo mobile.

Ogni volta che scorriamo, controlliamo se la visualizzazione è sufficientemente vicina alla fine della pista. In questo caso, estenderemo la sequenza spostando l'elemento sentinella e gli elementi che hanno lasciato l'area visibile nella parte inferiore della sequenza e li completeremo con nuovi contenuti.

Runway Sentinel Viewport

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

Tombstone

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

Una tomba simile. Molto roccioso. Wow.

Un aspetto interessante è che gli elementi 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 inviati dati e una tomba viene sostituita sopra la visualizzazione, ancorando la posizione di scorrimento a un elemento anziché a un valore in pixel. Questo concetto è chiamato ancoraggio allo scorrimento.

Ancoraggio scorrimento

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

Diagramma di ancoraggio dello scorrimento.

Se la visualizzazione della pagina viene ridimensionata e la passerella presenta modifiche, siamo in grado di ripristinare una situazione visivamente identica per l'utente. Vittoria! Tuttavia, una finestra di cui è stato modificato il formato indica che l'altezza di ogni elemento potrebbe essere cambiata, quindi come facciamo a sapere quanto in basso devono essere posizionati i contenuti ancorati? No. Per scoprirlo, dobbiamo eseguire il layout di ogni elemento sopra l'elemento ancorato e sommare tutte le altezze. Ciò potrebbe causare una pausa significativa dopo una modifica delle dimensioni, cosa che non vogliamo. Sfruttiamo invece l'assunto che ogni elemento sopra abbia le stesse dimensioni di una tomba e modifichiamo di conseguenza la posizione di scorrimento. Quando gli elementi vengono scorrevoli nella canalizzazione, modifichiamo la posizione di scorrimento, posticipando in modo efficace il lavoro di layout al momento del bisogno.

Layout

Ho saltato un dettaglio importante: il layout. Ogni riutilizzo di un elemento DOM solitamente comporta il nuovo layout dell'intera sequenza, il che ci farebbe scendere ben al di sotto del nostro obiettivo di 60 fotogrammi al secondo. Per evitare questo problema, ci assumiamo il compito di gestire il layout e utilizziamo elementi con posizionamento assoluto con trasformazioni. In questo modo possiamo fare finta che tutti gli elementi più avanti sulla passerella occupino ancora spazio, quando in realtà c'è solo spazio vuoto. Poiché il layout viene eseguito da noi, possiamo memorizzare nella cache le posizioni in cui termina ogni elemento e caricare immediatamente l'elemento corretto dalla cache quando l'utente scorre verso il basso.

Idealmente, gli elementi verrebbero ridipinti una sola volta quando vengono collegati al DOM e non sarebbero interessati dalle aggiunte o dalle rimozioni di altri elementi nella sequenza. È possibile, ma solo con i browser moderni.

Modifiche all'avanguardia

Di recente, Chrome ha aggiunto il supporto per il contenimento CSS, una funzionalità che consente a noi sviluppatori di indicare al browser che un elemento rappresenta un confine per il layout e la pittura. Poiché il layout è realizzato da noi, è un'ottima applicazione per il contenimento. Ogni volta che aggiungiamo un elemento alla passerella, sappiamo che gli altri elementi non devono essere interessati dal nuovo layout. Pertanto, ogni elemento deve essere contrassegnato come contain: layout. Inoltre, non vogliamo che il resto del sito web venga interessato, quindi anche la passerella stessa deve ricevere questa direttiva di stile.

Un'altra cosa che abbiamo preso in considerazione è l'utilizzo di IntersectionObservers come meccanismo per rilevare quando l'utente ha scremato abbastanza per consentirci di iniziare a riciclare gli elementi e caricare nuovi dati. Tuttavia, per gli IntersectionObserver è specificata una latenza elevata (come se si utilizzasse requestIdleCallback), quindi potremmo avere la sensazione che la risposta sia meno rapida con gli IntersectionObserver rispetto a non utilizzarli. Anche la nostra implementazione attuale che utilizza l'evento scroll soffre di questo problema, poiché gli eventi di scorrimento vengono inviati in base al criterio "best effort". Alla fine, il worklet Compositor di Houdini sarà la soluzione ad alta fedeltà a questo problema.

Non è ancora perfetto

La nostra attuale implementazione del riciclo del DOM non è ideale in quanto aggiunge tutti gli elementi che passano attraverso il viewport, anziché occuparsi solo di quelli effettivamente sulla schermata. Ciò significa che, quando scorri molto velocemente, Chrome deve gestire così tanto lavoro per il layout e la pittura che non riesce a stare al passo. Alla fine non vedrai altro che lo sfondo. Non è la fine del mondo, ma sicuramente qualcosa da migliorare.

Ci auguriamo che tu abbia compreso quanto possano essere complessi problemi semplici quando vuoi combinare un'esperienza utente eccezionale con standard di prestazioni elevati. Con le app web progressive che diventano esperienze di base sui cellulari, questo diventerà più importante e gli sviluppatori web dovranno continuare a investire nell'utilizzo di pattern che rispettino i vincoli di rendimento.

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