Uno sguardo all'interno del browser web moderno (parte 3)

Mariko Kosaka

Funzionamento interno di un processo di rendering

Questa è la terza parte di una serie di 4 post del blog che illustrano il funzionamento dei browser. In precedenza, abbiamo trattato l'architettura multiprocesso e il flusso di navigazione. In questo post esamineremo cosa succede all'interno del processo del renderer.

Il processo del renderer interessa molti aspetti del rendimento web. Poiché all'interno del processo di rendering avviene molto, questo post è solo una panoramica generale. Per approfondire, consulta la sezione sul rendimento di Web Fundamentals, che contiene molte altre risorse.

I processi di rendering gestiscono i contenuti web

Il processo del visualizzatore è responsabile di tutto ciò che accade all'interno di una scheda. In un processo di rendering, il thread principale gestisce la maggior parte del codice inviato all'utente. A volte parti del codice JavaScript vengono gestite da thread di lavoro se utilizzi un web worker o un service worker. I thread di compositore e raster vengono eseguiti anche all'interno di processi di rendering per eseguire il rendering di una pagina in modo efficiente e senza problemi.

Il compito principale del processo di rendering è trasformare HTML, CSS e JavaScript in una pagina web con cui l'utente può interagire.

Processo del renderer
Figura 1: processo di rendering con un thread principale, thread di lavoro, un thread compositor e un thread di rasterizzazione al suo interno

Analisi

Costruzione di un DOM

Quando il processo di rendering riceve un messaggio di commit per una navigazione e inizia a ricevere dati HTML, il thread principale inizia ad analizzare la stringa di testo (HTML) e a trasformarla in un Modelo di Objecti Documento (DOM).

Il DOM è la rappresentazione interna della pagina di un browser, nonché la struttura di dati e l'API con cui lo sviluppatore web può interagire tramite JavaScript.

L'analisi di un documento HTML in un DOM è definita dall'HTML Standard. Potresti aver notato che l'inserimento di HTML in un browser non genera mai un errore. Ad esempio, il tag di chiusura </p> mancante è un HTML valido. Il markup errato come Hi! <b>I'm <i>Chrome</b>!</i> (il tag b è chiuso prima del tag i) viene trattato come se avessi scritto Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Questo perché la specifica HTML è progettata per gestire questi errori in modo elegante. Se vuoi scoprire come vengono eseguite queste operazioni, consulta la sezione "Introduzione alla gestione degli errori e casi insoliti nel parser" della specifica HTML.

Caricamento delle risorse secondarie

Un sito web di solito utilizza risorse esterne come immagini, CSS e JavaScript. Questi file devono essere caricati dalla rete o dalla cache. Il thread principale potrebbe richiederli uno alla volta man mano che li trova durante l'analisi per creare un DOM, ma per velocizzare l'operazione, lo "scanner di precaricamento" viene eseguito in modo concorrente. Se nel documento HTML sono presenti elementi come <img> o <link>, lo scanner di precarica sbircia i token generati dall'interprete HTML e invia richieste al thread di rete nel processo del browser.

DOM
Figura 2: il thread principale esegue l'analisi del codice HTML e crea una struttura DOM

JavaScript può bloccare l'analisi

Quando l'analizzatore HTML trova un tag <script>, mette in pausa l'analisi del documento HTML e deve caricare, analizzare ed eseguire il codice JavaScript. Perché? Perché JavaScript può modificare la forma del documento utilizzando elementi come document.write() che modificano l'intera struttura DOM (la panoramica del modello di analisi nella specifica HTML contiene un bel diagramma). Questo è il motivo per cui l'interprete HTML deve attendere l'esecuzione di JavaScript prima di poter riprendere l'analisi del documento HTML. Se ti interessa sapere cosa succede durante l'esecuzione di JavaScript, il team di V8 ha parlato di questo argomento in diversi post del blog.

Suggerire al browser come caricare le risorse

Esistono molti modi in cui gli sviluppatori web possono inviare suggerimenti al browser per caricare le risorse in modo corretto. Se il codice JavaScript non utilizza document.write(), puoi aggiungere l'attributo async o defer al tag <script>. Il browser carica ed esegue il codice JavaScript in modo asincrono e non blocca l'analisi. Se opportuno, puoi anche utilizzare il modulo JavaScript. <link rel="preload"> è un modo per indicare al browser che la risorsa è necessaria per la navigazione corrente e che vuoi scaricarla il prima possibile. Per saperne di più, consulta Priorità delle risorse: come farsi aiutare dal browser.

Calcolo dello stile

Avere un DOM non è sufficiente per sapere come sarà la pagina perché possiamo applicare stili agli elementi della pagina in CSS. Il thread principale analizza il CSS e determina lo stile calcolato per ogni nodo DOM. Si tratta di informazioni sul tipo di stile applicato a ciascun elemento in base ai selettori CSS. Puoi visualizzare queste informazioni nella sezione computed di DevTools.

Stile calcolato
Figura 3: il thread principale analizza il CSS per aggiungere lo stile calcolato

Anche se non fornisci CSS, ogni nodo DOM ha uno stile calcolato. Il tag <h1> viene visualizzato più grande del tag <h2> e i margini sono definiti per ogni elemento. Questo accade perché il browser ha un foglio di stile predefinito. Se vuoi sapere come funziona il CSS predefinito di Chrome, puoi vedere il codice sorgente qui.

Layout

Ora il processo di rendering conosce la struttura di un documento e gli stili per ogni nodo, ma non è sufficiente per eseguire il rendering di una pagina. Immagina di provare a descrivere un dipinto a un amico al telefono. "C'è un grande cerchio rosso e un piccolo quadrato blu" non sono informazioni sufficienti per consentire al tuo amico di sapere esattamente come sarà il dipinto.

gioco di fax umano
Figura 4: una persona in piedi davanti a un dipinto, linea telefonica collegata all'altra persona

Il layout è un processo per trovare la geometria degli elementi. Il thread principale esamina il DOM e gli stili calcolati e crea la struttura ad albero del layout contenente informazioni come le coordinate x e y e le dimensioni della casella delimitante. L'albero del layout può avere una struttura simile all'albero DOM, ma contiene solo informazioni relative a ciò che è visibile nella pagina. Se viene applicato display: none, l'elemento non fa parte della struttura ad albero del layout (tuttavia, un elemento con visibility: hidden è presente nella struttura ad albero del layout). Analogamente, se viene applicato uno pseudo-elemento con contenuti come p::before{content:"Hi!"}, viene incluso nella struttura ad albero del layout anche se non è presente nel DOM.

layout
Figura 5: il thread principale che esamina l'albero DOM con gli stili calcolati e produce l'albero del layout
Figura 6: il layout della casella per un paragrafo spostato a causa della modifica dell'interruzione di riga

Determinare il layout di una pagina è un'operazione complessa. Anche il layout di pagina più semplice, come un flusso di blocchi dall'alto verso il basso, deve tenere conto delle dimensioni del carattere e di dove inserire i ritorni a capo perché influiscono sulle dimensioni e sulla forma di un paragrafo, il che influisce sulla posizione del paragrafo successivo.

Il CSS può far galleggiare un elemento su un lato, mascherare l'elemento in overflow e modificare le direzioni di scrittura. Come puoi immaginare, questa fase di layout ha un compito importante. In Chrome, un intero team di ingegneri lavora al layout. Se vuoi vedere i dettagli del loro lavoro, alcuni talk della conferenza BlinkOn sono registrati e molto interessanti da guardare.

Pittura

gioco di disegno
Figura 7: una persona davanti a una tela con in mano un pennello, che si chiede se debba prima disegnare un cerchio o un quadrato

La presenza di un DOM, uno stile e un layout non è ancora sufficiente per eseguire il rendering di una pagina. Supponiamo che tu stia cercando di riprodurre un dipinto. Conosci le dimensioni, la forma e la posizione degli elementi, ma devi ancora decidere in quale ordine dipingerli.

Ad esempio, z-index potrebbe essere impostato per determinati elementi. In questo caso, la visualizzazione in ordine degli elementi scritti in HTML comporterà un rendering errato.

Errore z-index
Figura 8: gli elementi della pagina vengono visualizzati in ordine di markup HTML, il che comporta un'immagine visualizzata errata perché l'indice z non è stato preso in considerazione

In questo passaggio di pittura, il thread principale esamina l'albero del layout per creare record di pittura. La registrazione della pittura è una nota del processo di pittura, ad esempio "prima lo sfondo, poi il testo e poi il rettangolo". Se hai disegnato sull'elemento <canvas> utilizzando JavaScript, questa procedura potrebbe esserti familiare.

record di Paint
Figura 9: il thread principale che attraversa l'albero del layout e produce record di pittura

L'aggiornamento della pipeline di rendering è costoso

Figura 10: alberi DOM+Style, Layout e Paint nell'ordine in cui vengono generati

L'aspetto più importante da comprendere nella pipeline di rendering è che a ogni passaggio il risultato dell'operazione precedente viene utilizzato per creare nuovi dati. Ad esempio, se qualcosa cambia nell'albero del layout, l'ordine di pittura deve essere rigenerato per le parti interessate del documento.

Se stai animando elementi, il browser deve eseguire queste operazioni tra ogni fotogramma. La maggior parte dei nostri display aggiorna lo schermo 60 volte al secondo (60 fps); l'animazione apparirà fluida agli occhi umani quando sposti elementi sullo schermo a ogni frame. Tuttavia, se nell'animazione mancano i fotogrammi intermedi, la pagina apparirà "a scatti".

Jitter causato da frame mancanti
Figura 11: frame di animazione in una sequenza temporale

Anche se le operazioni di rendering sono in linea con l'aggiornamento dello schermo, questi calcoli vengono eseguiti sul thread principale, il che significa che potrebbero essere bloccati quando l'applicazione esegue JavaScript.

Jage Jank di JavaScript
Figura 12: fotogrammi dell'animazione su una sequenza temporale, ma un fotogramma è bloccato da JavaScript

Puoi suddividere l'operazione JavaScript in piccoli blocchi e pianificarne l'esecuzione in ogni frame utilizzando requestAnimationFrame(). Per saperne di più su questo argomento, consulta Ottimizzare l'esecuzione di JavaScript. Puoi anche eseguire il codice JavaScript in Web Workers per evitare di bloccare il thread principale.

Richiedi frame dell&#39;animazione
Figura 13: piccoli blocchi di JavaScript in esecuzione su una sequenza temporale con frame di animazione

Composizione

Come disegneresti una pagina?

Figura 14: animazione del processo di rasterizzazione ingenuo

Ora che il browser conosce la struttura del documento, lo stile di ogni elemento, la geometria della pagina e l'ordine di disegno, come disegna una pagina? La conversione di queste informazioni in pixel sullo schermo è chiamata rasterizzazione.

Un modo ingenuo per gestire questo problema potrebbe essere rasterizzare le parti all'interno dell'area visibile. Se un utente scorra la pagina, sposta il frame rasterizzato e completa le parti mancanti eseguendo un'altra rasterizzazione. È così che Chrome gestiva la rasterizzazione quando è stato rilasciato per la prima volta. Tuttavia, il browser moderno esegue un processo più sofisticato chiamato composizione.

Che cos'è il compositing

Figura 15: animazione del processo di composizione

La composizione è una tecnica per separare le parti di una pagina in livelli, rasterizzarle separatamente e comporle come una pagina in un thread separato chiamato thread del compositore. Se si verifica lo scorrimento, poiché i livelli sono già rasterizzati, non resta che comporre un nuovo frame. L'animazione può essere ottenuta allo stesso modo spostando i livelli e componendo un nuovo frame.

Puoi vedere in che modo il tuo sito web è suddiviso in livelli in DevTools utilizzando il riquadro Livelli.

Suddivisione in livelli

Per scoprire quali elementi devono trovarsi in quali livelli, il thread principale esamina la struttura ad albero del layout per creare la struttura ad albero dei livelli (questa parte è chiamata "Aggiorna struttura ad albero dei livelli" nel riquadro sul rendimento di DevTools). Se alcune parti di una pagina che dovrebbero essere un livello separato (ad esempio il menu laterale scorrevole) non lo sono, puoi fornire un suggerimento al browser utilizzando l'attributo will-change in CSS.

albero dei livelli
Figura 16: il thread principale che attraversa l'albero del layout producendo l'albero dei livelli

Potresti essere tentato di assegnare livelli a ogni elemento, ma la composizione su un numero eccessivo di livelli potrebbe comportare un funzionamento più lento rispetto alla rasterizzazione di piccole parti di una pagina in ogni frame, pertanto è fondamentale misurare le prestazioni di rendering della tua applicazione. Per saperne di più sull'argomento, consulta Limitati alle proprietà solo per il compositore e gestisci il numero di livelli.

Rasterizzazione e composizione al di fuori del thread principale

Una volta creata la struttura ad albero dei livelli e determinati gli ordini di pittura, il thread principale esegue il commit di queste informazioni nel thread del compositore. Il thread del compositore esegue quindi la rasterizzazione di ogni livello. Un livello potrebbe essere grande come l'intera lunghezza di una pagina, quindi il thread del compositore li suddivide in riquadri e invia ogni riquadro ai thread di rasterizzazione. I thread rasterizzano ogni riquadro e lo memorizzano nella memoria della GPU.

raster
Figura 17: thread raster che creano la bitmap dei riquadri e la inviano alla GPU

Il thread del compositore può dare la priorità a diversi thread di rasterizzazione in modo che gli elementi all'interno del viewport (o nelle vicinanze) possano essere rasterizzati per primi. Un livello ha anche più suddivisioni per risoluzioni diverse per gestire, ad esempio, l'azione di zoom in.

Una volta rasterizzati, il thread del compositore raccoglie le informazioni sui riquadri di disegno chiamati draw quads per creare un frame del compositore.

Disegna quadrangoli Contiene informazioni come la posizione del riquadro in memoria e la posizione nella pagina in cui disegnare il riquadro tenendo conto del compositing della pagina.
Frame del compositore Una raccolta di quad di disegno che rappresenta un riquadro di una pagina.

Un frame del compositore viene quindi inviato al processo del browser tramite IPC. A questo punto, è possibile aggiungere un altro frame del compositore dal thread dell'interfaccia utente per la modifica dell'interfaccia utente del browser o da altre procedure di rendering per le estensioni. Questi frame del compositore vengono inviati alla GPU per essere visualizzati su uno schermo. Se viene ricevuto un evento di scorrimento, il thread del compositore crea un altro frame del compositore da inviare alla GPU.

composit
Figura 18: thread del compositore che crea il frame di composizione. Il frame viene inviato al processo del browser poi alla GPU

Il vantaggio del compositing è che viene eseguito senza coinvolgere il thread principale. Il thread del compositore non deve attendere il calcolo dello stile o l'esecuzione di JavaScript. Ecco perché le animazioni solo in compositing sono considerate le migliori per un'esecuzione fluida. Se è necessario calcolare di nuovo il layout o la pittura, deve essere coinvolto il thread principale.

Conclusione

In questo post abbiamo esaminato la pipeline di rendering dalla sintassi al compositing. Ora, spero, hai acquisito le informazioni necessarie per approfondire l'ottimizzazione del rendimento di un sito web.

Nel prossimo e ultimo post di questa serie, esamineremo il thread del compositore in modo più dettagliato e vedremo cosa succede quando vengono inseriti input utente come mouse move e click.

Ti è piaciuto il post? Se hai domande o suggerimenti per i post futuri, sarei felice di ricevere un tuo messaggio nella sezione dei commenti di seguito o su @kosamari su Twitter.

Passaggio successivo: l'input viene inviato al compositore