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

Mariko Kosaka

Funzionamento interno di un processo del renderer

Questa è la terza e quattro delle quattro della serie dedicata al funzionamento dei browser. In precedenza, abbiamo parlato dell'architettura multi-processo e del flusso di navigazione. In questo post vedremo cosa accade all'interno del processo di rendering.

Il processo del renderer riguarda molti aspetti delle prestazioni web. Poiché avvengono molte attività all'interno del processo di rendering, questo post offre una panoramica generale. Se vuoi saperne di più, la sezione Performance di Web Fundamentals ti offre molte altre risorse.

I processi del renderer gestiscono i contenuti web

Il processo di rendering è 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, se utilizzi un web worker o un service worker, alcune parti di JavaScript vengono gestite da thread di worker. I thread compositor e raster vengono inoltre eseguiti all'interno dei processi di un renderer per eseguire il rendering di una pagina in modo efficiente e fluido.

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 del renderer con un thread principale, thread worker, un thread del compositor e un thread raster all'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 Documento Model (DOM).

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

L'analisi di un documento HTML in un DOM è definita dallo standard HTML. Avrai notato che l'invio di codice HTML a un browser non genera mai un errore. Ad esempio, se manca il tag di chiusura </p> è un codice HTML valido. Il markup errato come Hi! <b>I'm <i>Chrome</b>!</i> (il tag b viene 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 controllato. Se vuoi sapere come vengono eseguite queste operazioni, puoi leggere la sezione "Un'introduzione alla gestione degli errori e ai casi strani nel parser" delle specifiche HTML.

Caricamento sottorisorse

In genere un sito web 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 mentre li trova durante l'analisi per creare un DOM, ma per velocizzare il processo viene eseguito contemporaneamente lo "scanner di precaricamento". Se nel documento HTML sono presenti elementi come <img> o <link>, lo scanner di precaricamento esamina i token generati dall'analizzatore sintattico HTML e invia richieste al thread di rete durante il processo del browser.

DOM
Figura 2: il thread principale che analizza il codice HTML e crea un albero DOM

JavaScript può bloccare l'analisi

Quando il parser HTML trova un tag <script>, mette in pausa l'analisi del documento HTML e deve caricare, analizzare ed eseguire il codice JavaScript. Perché JavaScript può cambiare la forma del documento utilizzando elementi come document.write(), che modifica l'intera struttura DOM (la panoramica del modello di analisi nella specifica HTML offre un diagramma interessante). Per questo motivo, l'analizzatore sintattico HTML deve attendere l'esecuzione di JavaScript prima di poter riprendere l'analisi del documento HTML. Se vuoi sapere cosa succede nell'esecuzione di JavaScript, il team V8 pubblica dibattiti e post del blog.

Suggerisci al browser come caricare le risorse

Gli sviluppatori web possono inviare suggerimenti al browser in molti modi per caricare correttamente le risorse. Se 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"> consente di comunicare al browser che la risorsa è assolutamente necessaria per la navigazione corrente e vuoi scaricarla il prima possibile. Per saperne di più, consulta l'articolo Assegnazione delle priorità alle risorse – Ottenere l'aiuto del browser.

Calcolo dello stile

Disporre di un DOM non è sufficiente per capire quale sarebbe l'aspetto della pagina, perché possiamo applicare uno stile agli elementi della pagina in CSS. Il thread principale analizza il codice CSS e determina lo stile calcolato per ciascun nodo DOM. Queste sono informazioni sul tipo di stile applicato a ogni elemento in base ai selettori CSS. Puoi visualizzare queste informazioni nella sezione computed di DevTools.

Stile elaborato
Figura 3: il thread principale di analisi dei CSS per aggiungere uno stile calcolato

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

Layout

Ora il processo di rendering conosce la struttura di un documento e gli stili per ogni nodo, ma ciò non è sufficiente per eseguire il rendering di una pagina. Immagina di cercare di descrivere un dipinto a un amico al telefono. "C'è un grande cerchio rosso e un piccolo quadrato blu" non sono sufficienti per far capire al tuo amico che aspetto avrebbe esattamente il dipinto.

gioco del fax umano
Figura 4: una persona in piedi di fronte a un dipinto, con una linea telefonica connessa all'altra persona

Il layout è un processo che consente di trovare la geometria degli elementi. Il thread principale esplora il DOM e gli stili calcolati e crea la struttura ad albero del layout che contiene informazioni come le coordinate x y e le dimensioni del riquadro di delimitazione. La struttura ad albero del layout può avere una struttura simile a quella della struttura DOM, ma contiene solo informazioni relative a ciò che è visibile nella pagina. Se viene applicato display: none, questo elemento non fa parte della struttura ad albero del layout (tuttavia, un elemento con visibility: hidden si trova nell'albero del layout). Analogamente, se viene applicata una pseudoclasse con contenuti come p::before{content:"Hi!"}, questa viene inclusa nella struttura del layout anche se non si trova nel DOM.

layout
Figura 5: il thread principale che passa sopra l'albero DOM con stili calcolati e produce l'albero di layout
Figura 6: layout a riquadro per un paragrafo che si sposta a causa di un'interruzione di riga

Determinare il layout di una pagina è un compito impegnativo. Anche il layout di pagina più semplice, come un blocco a blocchi dall'alto verso il basso, deve considerare le dimensioni del carattere e la posizione delle interruzioni di riga, perché queste incidono sulle dimensioni e sulla forma di un paragrafo, il che a sua volta incide sulla posizione del paragrafo successivo.

CSS può far fluttuare l'elemento su un lato, mascherare l'elemento extra e modificare le direzioni di scrittura. Come si può immaginare, questa fase di layout ha un compito arduo. In Chrome, un intero team di ingegneri lavora al layout. Se vuoi vedere i dettagli del loro lavoro, vengono registrati alcuni discorsi della BlinkOn Conference e sono piuttosto interessanti da guardare.

Verniciatura

gioco di disegno
Figura 7: una persona di fronte a una tela che tiene un pennello e chiede se deve prima disegnare un cerchio o un quadrato

Avere 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 comunque valutare l'ordine in cui dipingerli.

Ad esempio, per alcuni elementi potrebbe essere impostato z-index, in questo caso il disegno in ordine di elementi scritti nel codice HTML comporterà un rendering errato.

errore z-index
Figura 8: gli elementi di pagina vengono visualizzati in ordine di markup HTML e generano un'immagine visualizzata in modo errato, poiché lo z-index non è stato preso in considerazione

In questa fase di colorazione, il thread principale percorre l'albero del layout per creare record di colorazione. "Pittura record" è una nota relativa al processo di pittura, come "prima lo sfondo, poi il testo, quindi il rettangolo". Se hai disegnato un elemento <canvas> usando JavaScript, questo processo potrebbe esserti familiare.

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

L'aggiornamento della pipeline di rendering è costoso

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

La cosa 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 nella struttura ad albero del layout, è necessario rigenerare l'ordine di colorazione per le parti interessate del documento.

Se stai animando gli elementi, il browser deve eseguire queste operazioni tra un frame e l'altro. La maggior parte dei nostri display aggiorna lo schermo 60 volte al secondo (60 f/s); l'animazione risulta fluida agli occhi umani quando muovi gli oggetti sullo schermo a ogni fotogramma. Tuttavia, se nell'animazione mancano i frame intermedi, la pagina avrà un aspetto "insoddisfacente".

jage jank per frame mancanti
Figura 11: frame di animazione su una sequenza temporale

Anche se le tue operazioni di rendering sono al passo con l'aggiornamento dello schermo, questi calcoli vengono eseguiti sul thread principale, il che significa che potrebbe essere bloccato quando la tua applicazione esegue JavaScript.

jage jank tramite JavaScript
Figura 12: frame dell'animazione su una sequenza temporale, ma un frame è bloccato da JavaScript

Puoi dividere l'operazione JavaScript in piccoli blocchi e pianificarne l'esecuzione a ogni frame utilizzando requestAnimationFrame(). Per ulteriori informazioni su questo argomento, consulta Ottimizzare l'esecuzione di JavaScript. Potresti anche eseguire JavaScript in Web Workers per evitare di bloccare il thread principale.

richiedi frame animazione
Figura 13: blocchi più piccoli di JavaScript in esecuzione su una sequenza temporale con frame di animazione

Composizione

Come disegneresti una pagina?

Figura 14: animazione di un processo di rastering ingenuo

Ora che il browser conosce la struttura del documento, lo stile di ogni elemento, la geometria della pagina e l'ordine di colorazione, come fa a disegnare una pagina? La trasformazione di queste informazioni in pixel sullo schermo si chiama rasterizzazione.

Forse un modo ingenuo di gestire questo aspetto potrebbe essere quello di raster delle parti all'interno dell'area visibile. Se un utente scorre la pagina, sposta il frame rasterizzato e riempi le parti mancanti con ulteriori raster. Questo è il modo in cui Chrome ha gestito il rasterizzazione quando è stato rilasciato per la prima volta. Tuttavia, il browser moderno esegue un processo più sofisticato chiamato compositing.

Che cos'è la composizione

Figura 15: animazione del processo di composizione

La composizione è una tecnica per separare parti di una pagina in livelli, rasterizzarle separatamente e compositare come una pagina in un thread separato chiamato thread compositor. Se si attiva lo scorrimento, poiché i livelli sono già rasterizzati, non devi fare altro che comporre un nuovo fotogramma. Puoi ottenere l'animazione nello stesso modo spostando i livelli e componendo un nuovo fotogramma.

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

Divisione in livelli

Per scoprire quali elementi devono trovarsi in quali livelli, il thread principale attraversa l'albero del layout per creare l'albero dei livelli (questa parte è chiamata "Aggiorna albero dei livelli" nel riquadro delle prestazioni di DevTools). Se alcune parti di una pagina che devono essere un livello separato (come il menu laterale a scorrimento) non ne ricevono uno, 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 avere la tentazione di assegnare livelli a ogni elemento, ma la composizione su un numero eccessivo di livelli potrebbe comportare un'operazione più lenta rispetto alla rasterizzazione di piccole parti di una pagina in ogni frame, quindi è fondamentale misurare le prestazioni di rendering dell'applicazione. Per ulteriori informazioni sull'argomento, consulta Attieniti alle proprietà solo composito e gestisci il conteggio livelli.

Raster e composito fuori dal thread principale

Dopo aver creato l'albero dei livelli e aver determinato gli ordini di colorazione, il thread principale esegue il commit di queste informazioni nel thread del compositore. Il thread del compositore rasterizza quindi ogni livello. Un livello può essere grande quanto l'intera lunghezza di una pagina, quindi il thread del compositore li divide in riquadri e invia ogni riquadro a thread raster. I thread raster rasterizzano ogni riquadro e li archiviano nella memoria GPU.

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

Il thread compositor può dare la priorità a diversi thread raster in modo che gli elementi all'interno dell'area visibile (o nelle vicinanze) possano essere raster per primi. Un livello ha anche più riquadri per diverse risoluzioni per gestire azioni come lo zoom in avanti.

Una volta che i riquadri sono stati rasterati, il thread del compositore raccoglie informazioni sui riquadri chiamate draw quad per creare un frame composito.

Disegna quad Contiene informazioni quali la posizione del riquadro in memoria e la posizione nella pagina in cui tracciare il riquadro, prendendo in considerazione la composizione della pagina.
Frame compositore Una raccolta di quadricipiti che rappresentano un frame di una pagina.

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

composito
Figura 18: thread compositore che crea frame di composizione. Il frame viene inviato al processo del browser, quindi alla GPU

Il vantaggio della composizione è che viene eseguita senza coinvolgere il thread principale. Il thread compositore non deve attendere il calcolo dello stile o l'esecuzione di JavaScript. Questo è il motivo per cui la composizione solo di animazioni sono considerate la migliore per prestazioni fluide. Se occorre calcolare di nuovo il layout o la colorazione, è necessario coinvolgere il thread principale.

Conclusione

In questo post, abbiamo esaminato la pipeline di rendering dall'analisi alla composizione. Speriamo che ora tu abbia la possibilità di saperne di più sull'ottimizzazione del rendimento di un sito web.

Nel prossimo e ultimo post di questa serie, esamineremo più in dettaglio il thread del compositore e vedremo cosa succede quando entra in gioco un input utente come mouse move e click.

Ti è piaciuto il post? Per eventuali domande o suggerimenti per il prossimo post, mi piacerebbe ricevere la tua opinione nella sezione dei commenti qui sotto o @kosamari su Twitter.

Successivo: l'input sarà in arrivo per il compositore