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.
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.
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.
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.
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.
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
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.
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.
L'aggiornamento della pipeline di rendering è costoso
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".
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.
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.
Composizione
Come disegneresti una pagina?
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
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.
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.
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.
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.