In breve: riutilizza gli elementi DOM e rimuovi quelli lontani dalla viewport. Utilizza i segnaposto per tenere conto dei dati ritardati. Ecco una demo e il codice per lo scroller infinito.
I siti con scorrimento infinito spuntano ovunque su internet. L'elenco degli artisti di Google Music è uno, la cronologia di Facebook è uno e il feed live di Twitter è uno. Scorri verso il basso e, prima di raggiungere la fine della pagina, nuovi contenuti appaiono magicamente dal nulla. È un'esperienza senza interruzioni per gli utenti ed è facile capire il motivo del ricorso.
La sfida tecnica che si cela dietro uno scorrimento infinito, tuttavia, è più difficile di quanto sembra. La gamma di problemi che incontri quando vuoi fare la cosa giusta™ è vasta. Si inizia con cose semplici come i link nel piè di pagina che diventano praticamente irraggiungibili perché i contenuti continuano a spostare il piè di pagina. Ma i problemi diventano più difficili. Come gestisci un evento di ridimensionamento quando un utente ruota lo smartphone da verticale a orizzontale o come impedisci allo smartphone di bloccarsi quando l'elenco diventa troppo lungo?
The right thing™
Abbiamo pensato che fosse un 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, indicatori di eliminazione e ancoraggio dello scorrimento.
Il nostro caso dimostrativo sarà una finestra di chat simile a Hangouts in cui possiamo scorrere i messaggi. La prima cosa di cui abbiamo bisogno è una fonte infinita di messaggi di chat. Tecnicamente, nessuno degli scorrimenti infiniti esistenti è veramente infinito, ma con la quantità di dati disponibili da inserire in questi scorrimenti, potrebbero esserlo. Per semplicità, codificheremo un insieme di messaggi di chat e sceglieremo in modo casuale messaggio, autore e allegato di immagini occasionale, con un pizzico di ritardo artificiale per comportarci in modo più simile alla rete reale.
Riciclo del DOM
Il riciclo del DOM è una tecnica sottoutilizzata per mantenere basso il conteggio dei nodi DOM. L'idea generale è di utilizzare elementi DOM già creati che non sono visibili sullo schermo anziché crearne di nuovi. A dire il vero, i nodi DOM in sé sono economici, ma non sono senza costi, in quanto ognuno di essi aggiunge costi aggiuntivi 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 riorganizzazione e riapplicazione degli stili, un processo 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 ci consente di mantenere il numero totale di nodi DOM notevolmente inferiore, velocizzando tutti questi processi.
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 px per 1 px con una trasformazione per forzare l'elemento che contiene gli elementi, ovvero la pista, ad avere l'altezza desiderata. Promuoveremo ogni elemento della pista al proprio livello per assicurarci che il livello della pista stesso 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 nostra 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 si è avvicinata sufficientemente alla fine della pista. In questo caso, estenderemo la pista spostando l'elemento sentinella e spostando gli elementi che hanno lasciato la visualizzazione nella parte inferiore della pista e riempiendoli con nuovi contenuti.
Lo stesso vale per lo scorrimento nell'altra direzione. Tuttavia, non ridurremo mai la pista di atterraggio nella nostra implementazione, in modo che la posizione della barra di scorrimento rimanga coerente.
Tombstones
Come abbiamo detto in precedenza, cerchiamo di far sì 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 cui disponiamo di dati. In questo caso, inseriremo un elemento segnaposto che verrà sostituito dall'elemento con i contenuti effettivi una volta arrivati i dati. Anche i segnali di eliminazione vengono riciclati e hanno un pool separato per gli elementi DOM riutilizzabili. Ci serve per poter effettuare una transizione fluida da un segnaposto all'elemento compilato con i contenuti, altrimenti l'utente potrebbe perdere il filo del discorso.
Una sfida interessante è che gli elementi reali possono avere un'altezza maggiore rispetto all'elemento segnaposto a causa di quantità diverse di testo per elemento o di un'immagine allegata. Per risolvere il problema, modificheremo la posizione di scorrimento attuale ogni volta che arrivano dati e un segnaposto viene sostituito sopra la visualizzazione, ancorando la posizione di scorrimento a un elemento anziché a un valore in pixel. Questo concetto è chiamato ancoraggio dello scorrimento.
Ancoraggio dello scorrimento
L'ancoraggio dello scorrimento viene richiamato sia quando vengono sostituite le lapidi sia quando viene ridimensionata la finestra (cosa che accade anche quando il dispositivo viene capovolto). Dobbiamo capire qual è l'elemento più in alto visibile 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.
Se le dimensioni della finestra vengono modificate e la pista ha subito cambiamenti, siamo in grado di ripristinare una situazione che l'utente percepisce come visivamente identica. Vittoria! Tranne che una finestra ridimensionata significa che l'altezza di ogni elemento è potenzialmente cambiata, quindi come facciamo a sapere a quale distanza verso il basso deve essere posizionato il contenuto ancorato? Non lo facciamo. Per scoprirlo, dovremmo disporre ogni elemento sopra l'elemento ancorato e sommare tutte le loro altezze. Ciò potrebbe causare una pausa significativa dopo un ridimensionamento, e non vogliamo che ciò accada. Invece, presumiamo che ogni elemento sopra abbia le stesse dimensioni di una lapide e regoliamo di conseguenza la posizione di scorrimento. Man mano che gli elementi vengono visualizzati, regoliamo la posizione di scorrimento, posticipando di fatto il lavoro di layout al momento in cui è effettivamente necessario.
Layout
Ho saltato un dettaglio importante: il layout. Ogni riciclo di un elemento DOM normalmente riorganizzerebbe l'intero runway, il che ci porterebbe ben al di sotto del nostro obiettivo di 60 fotogrammi al secondo. Per evitare questo problema, ci assumiamo l'onere del layout e utilizziamo elementi con posizionamento assoluto con trasformazioni. In questo modo possiamo fingere che tutti gli elementi più avanti sulla pista stiano ancora occupando spazio quando in realtà c'è solo spazio vuoto. Poiché eseguiamo il layout autonomamente, possiamo memorizzare nella cache le posizioni in cui finisce ogni elemento e possiamo caricare immediatamente l'elemento corretto dalla cache quando l'utente scorre all'indietro.
Idealmente, gli elementi vengono ridisegnati una sola volta quando vengono collegati al DOM e non vengono influenzati dall'aggiunta o dalla rimozione di altri elementi nella striscia. È 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 agli sviluppatori di comunicare al browser che un elemento è un limite per
il layout e la pittura. Poiché qui eseguiamo il layout autonomamente, si tratta di un'applicazione
ideale per il contenimento. Ogni volta che aggiungiamo un elemento alla pista, sappiamo
che gli altri elementi non devono essere interessati dalla riorganizzazione. Pertanto, ogni elemento deve
ottenere contain: layout. Inoltre, non vogliamo influire sul resto del nostro sito web,
quindi anche la pista deve ricevere questa direttiva di stile.
Un altro aspetto che abbiamo preso in considerazione è l'utilizzo di
IntersectionObservers come meccanismo per rilevare quando
l'utente ha scorre abbastanza a lungo da consentirci di iniziare a riciclare gli elementi e caricare nuovi
dati. Tuttavia, gli IntersectionObserver sono specificati per avere una latenza elevata (come se
utilizzassero requestIdleCallback), quindi potremmo effettivamente percepire una reattività inferiore con
gli IntersectionObserver rispetto a senza. Anche la nostra attuale implementazione che utilizza l'evento
scroll soffre di questo problema, poiché gli eventi di scorrimento vengono inviati in base al
"miglior sforzo". Alla fine, Houdini's Compositor Worklet
sarebbe 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 la finestra, anziché occuparsi solo di quelli che sono effettivamente sullo schermo. Ciò significa che quando scorri molto velocemente, Chrome deve lavorare così tanto per il layout e la pittura che non riesce a tenere il passo. e finirai per vedere solo lo sfondo. Non è la fine del mondo, ma sicuramente qualcosa da migliorare.
Ci auguriamo che tu abbia capito quanto possono diventare difficili problemi semplici quando vuoi combinare un'ottima esperienza utente con standard di prestazioni elevati. Con le app web progressive che diventano esperienze principali 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'utilizzo principale è didattico.