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

Mariko Kosaka

Funzionamento interno di un processo di rendering

Questa è la terza di quattro serie di blog che illustra il funzionamento dei browser. In precedenza, abbiamo parlato dell'architettura multi-processo e del flusso di navigazione. In questo post esamineremo cosa succede all'interno del processo del renderer.

Il processo del renderer riguarda molti aspetti delle prestazioni web. Dato che il processo di rendering presenta molte cose, questo post è solo una panoramica generale. Se vuoi saperne di più, la sezione Prestazioni di Web Fundamentals offre molte altre risorse.

I processi del renderer gestiscono i contenuti web

Il processo del renderer è responsabile di tutto ciò che accade all'interno di una scheda. In un processo del renderer, il thread principale gestisce la maggior parte del codice che invii all'utente. A volte, alcune parti del codice JavaScript vengono gestite da thread worker se utilizzi un worker web o un service worker. Anche i thread di compositore e raster vengono eseguiti all'interno dei processi di un renderer per eseguire il rendering di una pagina in modo efficiente e fluido.

Il compito di base del processo di rendering è quello di 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 di lavoro, un thread del compositore e un thread raster all'interno

Analisi in corso...

Costruzione di un DOM

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

Il DOM è la rappresentazione interna della pagina del 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 </p> di chiusura è 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 considerato come se avessi scritto Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Il motivo è che la specifica HTML è progettata per gestire agevolmente questi errori. Se vuoi sapere come vengono svolte queste operazioni, puoi leggere la sezione "Introduzione alla gestione degli errori e casi insoliti nell'analizzatore sintattico" delle specifiche HTML.

Caricamento delle 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 quando li trova durante l'analisi per creare un DOM, ma per velocizzare il processo, viene eseguito contemporaneamente il "precaricamento dello scanner". Se il documento HTML contiene elementi come <img> o <link>, lo scanner di precaricamento controlla i token generati dall'analizzatore sintattico HTML e invia le richieste al thread di rete nel processo del browser.

DOM
Figura 2: thread principale che analizza l'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é? perché JavaScript può modificare la forma del documento utilizzando elementi come document.write(), che modifica l'intera struttura DOM (panoramica del modello di analisi nella specifica HTML ha un diagramma utile). Questo è il motivo per cui il parser HTML deve attendere l'esecuzione di JavaScript prima di poter riprendere l'analisi del documento HTML. Se sei curioso di sapere cosa succede nell'esecuzione di JavaScript, il team di V8 ha pubblicato conferenze e post del blog in merito.

Suggerimento al browser su come caricare le risorse

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

Calcolo dello stile

Avere un DOM non è sufficiente per sapere come sarebbe la 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 le 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 codice CSS di analisi del thread principale per aggiungere lo stile calcolato

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

Layout

Ora il processo del renderer conosce la struttura di un documento e gli stili per ciascun nodo, ma questo non è sufficiente per eseguire il rendering di una pagina. Immagina di voler descrivere un quadro al tuo amico tramite uno smartphone. "Ci sono un grande cerchio rosso e un piccolo quadrato blu" non sono informazioni 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 quadro, la linea telefonica collegata all'altra persona

Il layout è un processo per trovare la geometria degli elementi. Il thread principale analizza il DOM e gli stili calcolati e crea un albero del layout che contiene informazioni come le coordinate x y e le dimensioni dei riquadri di delimitazione. L'albero del layout potrebbe avere una struttura simile all'albero DOM, ma contiene solo informazioni relative a ciò che è visibile sulla pagina. Se viene applicato display: none, l'elemento non fa parte dell'albero del layout, ma c'è un elemento con visibility: hidden nell'albero del layout. Allo stesso modo, se viene applicato uno pseudo-elemento con contenuti come p::before{content:"Hi!"}, questo viene incluso nell'albero del layout anche se non si trova nel DOM.

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

Determinare il layout di una pagina è un compito impegnativo. Anche il layout di pagina più semplice, come un flusso a blocchi dall'alto verso il basso, deve considerare le dimensioni del carattere e il punto in cui spezzarle, perché incidono sulla dimensione e sulla forma del paragrafo, che a sua volta determina dove deve essere il paragrafo successivo.

Il CSS può far fluttuare l'elemento su un lato, mascherare l'elemento extra e modificare le direzioni di scrittura. Questa fase di layout ha un compito arduo. In Chrome, un intero team di tecnici si occupa del layout. Se vuoi vedere i dettagli del loro lavoro, vengono registrate alcune conferenze della BlinkOn Conference che sono piuttosto interessanti da guardare.

Verniciatura

gioco di disegno
Figura 7: una persona di fronte a una tela con un pennello che si chiede se debba 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 quadro. Conosci le dimensioni, la forma e la posizione degli elementi, ma devi comunque giudicare l'ordine in cui li dipingi.

Ad esempio, per alcuni elementi potrebbe essere impostato z-index. In questo caso, la pittura in base agli elementi scritti nel codice HTML comporterà il rendering errato.

z-index non riuscito
Figura 8: elementi della pagina visualizzati in ordine di markup HTML, con conseguente rendering errato dell'immagine perché z-index non è stato preso in considerazione

In questa fase di colorazione, il thread principale segue l'albero del layout per creare record di colorazione. Il record di colorazione è una nota del processo di pittura come "prima lo sfondo, poi il testo, poi il rettangolo". Se hai disegnato sull'elemento <canvas> utilizzando JavaScript, potresti conoscere questa procedura.

dipinto record
Figura 9: il thread principale che cammina nell'albero dei layout e produce record sui colori

L'aggiornamento della pipeline di rendering è costoso

Figura 10: DOM+Stile, Layout e Struttura ad albero in ordine di generazione

La cosa più importante da comprendere nella pipeline di rendering è che in 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 elementi, il browser deve eseguire queste operazioni tra ogni frame. La maggior parte dei display aggiorna lo schermo 60 volte al secondo (60 f/s); l'animazione risulterà fluida agli occhi umani quando muovi i contenuti sullo schermo a ogni frame. Tuttavia, se nell'animazione mancano i frame intermedi, la pagina risulterà "instabile".

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

Anche se le operazioni di rendering mantengono il passo 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: frame di 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. Puoi anche eseguire il codice JavaScript in Web worker per evitare di bloccare il thread principale.

richiedi frame dell&#39;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 è chiamata rasterizzazione.

Forse un modo ingenuo per gestire questo problema potrebbe essere quello di eseguire una rasterizzazione delle parti all'interno dell'area visibile. Se un utente scorre la pagina, sposta il frame raster e completa le parti mancanti eseguendo ulteriori operazioni di rasterizzazione. Chrome ha gestito la 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 le parti di una pagina in livelli, rasterizzarle separatamente e comporle come una pagina in un thread separato chiamato thread del compositore. Se avviene lo scorrimento, poiché i livelli sono già rasterizzati, deve solo comporre un nuovo frame. È possibile creare l'animazione allo stesso modo spostando i livelli e creando un nuovo frame.

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

Suddivisione in strati

Per individuare gli elementi da inserire in quali livelli, il thread principale passa nella struttura ad albero del layout per creare la struttura dei livelli (questa parte è chiamata "Update Layer Tree" (Aggiorna albero dei livelli) nel riquadro delle prestazioni di DevTools. Se alcune parti di una pagina che dovrebbero essere un livello separato (come il menu laterale a scorrimento in entrata) non ne ricevono una, puoi dare un suggerimento al browser utilizzando l'attributo will-change in CSS.

albero dei livelli
Figura 16: il thread principale che cammina nell'albero di layout che produce l'albero dei livelli

Potresti avere la tentazione di assegnare livelli a ogni elemento, ma la composizione su un numero eccessivo di strati potrebbe rallentare le operazioni rispetto alla rasterizzazione di piccole parti di una pagina per frame, quindi è fondamentale misurare le prestazioni di rendering dell'applicazione. Per maggiori informazioni sull'argomento, consulta la sezione Attieniti alle proprietà solo compositore e Gestisci il conteggio dei livelli.

Raster e compositi a partire dal thread principale

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

raster
Figura 17: thread raster durante la creazione della bitmap dei riquadri e l'invio alla GPU

Il thread del compositore può dare la priorità a diversi thread raster in modo che gli elementi all'interno dell'area visibile (o nelle vicinanze) possano essere rasterizzati per primi. Un livello ha anche più riquadri per risoluzioni diverse al fine di gestire aspetti come l'aumento dello zoom.

Dopo aver rasterizzato i riquadri, il thread del compositore raccoglie informazioni sui riquadri chiamate quad di disegno per creare un frame composito.

Disegna quadri Contiene informazioni quali la posizione in memoria del riquadro e la posizione nella pagina in cui tracciare il riquadro, tenendo conto della composizione della pagina.
Frame del compositore Una raccolta di quadri di disegno che rappresenta un frame di una pagina.

Un frame del compositore viene quindi inviato al processo del browser tramite IPC. A questo punto, potrebbe essere aggiunto un altro frame del 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 del compositore vengono inviati alla GPU per visualizzarli su uno schermo. Se arriva un evento di scorrimento, il thread del compositore crea un altro frame del compositore da inviare alla GPU.

composit
Figura 18: thread del compositore durante la creazione di un 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 del compositore non deve attendere il calcolo dello stile o l'esecuzione di JavaScript. Questo è il motivo per cui la composizione delle sole animazioni è considerata la migliore per ottenere prestazioni fluide. Se occorre calcolare di nuovo il layout o la colorazione, occorre coinvolgere il thread principale.

Conclusione

In questo post, abbiamo esaminato la pipeline di rendering, dall'analisi alla compositing. Speriamo che ora tu abbia la possibilità di scoprire di più sull'ottimizzazione delle prestazioni di un sito web.

Nel prossimo e ultimo post di questa serie, daremo un'occhiata al thread del compositore in maggiore dettaglio e vedremo cosa succede quando arriva un input utente come mouse move e click.

Ti è piaciuto il post? Se hai domande o suggerimenti per il prossimo post, mi piacerebbe sentire la tua nella sezione dei commenti qui sotto o @kosamari su Twitter.

Successivo: l'input è in arrivo al compositore