Molte delle ottimizzazioni di cui si parla nelle Web Performance, dal CSS critico al differimento degli script, dalla priorità delle immagini al preload dei font, hanno senso solo se si capisce un processo di fondo: il modo in cui il browser trasforma un documento HTML, qualche foglio di stile e alcuni script nei pixel che compaiono sullo schermo. Questo processo si chiama percorso critico di rendering, e conoscerlo cambia il modo di guardare una pagina lenta: invece di un elenco di consigli da applicare, diventa chiaro perché certe risorse fanno aspettare l’utente e altre no.
In questo articolo lo ripercorriamo passo per passo, senza dare nulla per scontato: cosa fa il browser con l’HTML, cosa con il CSS, dove interviene il JavaScript, quali sono le fasi che portano al disegno e cosa blocca il primo disegno. Alla fine, il legame con metriche come FCP e LCP sarà evidente.
Dall’HTML al DOM
Tutto comincia con il documento HTML. Il browser lo riceve come una sequenza di byte e lo legge progressivamente, man mano che arriva, senza aspettare di averlo tutto. Durante la lettura riconosce i tag, li trasforma in nodi e costruisce una struttura ad albero che rappresenta la pagina: il DOM, Document Object Model. Ogni elemento, dal titolo al paragrafo, dall’immagine al pulsante, diventa un nodo di questo albero, con i propri attributi e i propri figli.
Mentre legge, il browser incontra riferimenti ad altre risorse: fogli di stile, script, immagini, font. Un componente dedicato, il preload scanner, scorre il documento in anticipo per scoprirle il prima possibile e avviarne il download in parallelo. È uno dei motivi per cui una risorsa dichiarata direttamente nell’HTML viene scaricata molto prima di una inserita da un altro file o da uno script.
Dal CSS al CSSOM
In parallelo il browser elabora i fogli di stile e costruisce una seconda struttura, il CSSOM, CSS Object Model, che contiene tutte le regole di stile con le loro priorità. Qui c’è una differenza importante rispetto all’HTML: il CSS non può essere applicato a pezzi. Una regola alla fine del foglio può sovrascrivere una regola all’inizio, per cui il browser deve avere l’intero foglio di stile prima di poter stabilire con certezza come appare un elemento.
Per questo i fogli di stile caricati nell’intestazione della pagina bloccano il rendering: finché non sono arrivati ed elaborati, il browser non disegna nulla. È una scelta deliberata, per evitare di mostrare la pagina senza stili e poi ridisegnarla, con un effetto sgradevole per l’utente. Ma significa che ogni kilobyte di CSS caricato in modo bloccante si trova sul percorso del primo disegno.
Il ruolo del JavaScript
Il JavaScript complica il quadro, perché può fare quasi tutto: leggere e modificare il DOM, aggiungere o togliere elementi, cambiare gli stili, perfino scrivere nuovo HTML nel documento. Per questo, quando il browser incontra uno script normale durante la lettura dell’HTML, si ferma: deve scaricarlo, se è esterno, ed eseguirlo prima di proseguire, perché lo script potrebbe cambiare ciò che viene dopo. E poiché uno script può leggere gli stili degli elementi, il browser deve anche aver completato i fogli di stile precedenti prima di eseguirlo.
Uno script sincrono nell’intestazione, quindi, blocca la costruzione del DOM, attende il CSS e occupa il main thread con la propria esecuzione, tutto prima che l’utente veda qualcosa. Gli attributi defer e async cambiano questo comportamento: con defer lo script viene scaricato in parallelo ed eseguito solo dopo la lettura completa del documento, nell’ordine in cui compare; con async viene eseguito appena scaricato, senza un ordine garantito. Gli script di tipo modulo si comportano per impostazione predefinita come quelli differiti.
<!-- Blocca la lettura del documento finché non è scaricato ed eseguito -->
<script src="/js/app.js"></script>
<!-- Scaricato in parallelo, eseguito dopo la lettura del documento -->
<script src="/js/app.js" defer></script>
<!-- Scaricato in parallelo, eseguito appena pronto -->
<script src="/js/statistiche.js" async></script>
Render tree, layout, paint e composite
Quando DOM e CSSOM sono pronti, il browser li combina nel render tree, l’albero di rendering, che contiene solo gli elementi da mostrare con i loro stili calcolati. Gli elementi nascosti con display: none, per esempio, non ne fanno parte.
Segue il layout: il browser calcola la posizione e la dimensione esatta di ogni elemento, in base alla larghezza dello schermo, ai testi, alle immagini, alle regole di disposizione. È un calcolo che dipende dalle relazioni tra gli elementi, e il suo costo cresce con la dimensione e la complessità della pagina, come abbiamo visto parlando del costo del DOM nei page builder.
Poi il paint, il disegno vero e proprio: il browser produce i pixel di testi, colori, bordi, ombre e immagini. Infine il composite: la pagina può essere divisa in livelli, per esempio per elementi animati o con posizionamento fisso, che vengono disegnati separatamente e poi combinati, spesso con l’aiuto della scheda grafica. Separare i livelli permette di aggiornare una parte della pagina senza ridisegnare tutto il resto.
Queste fasi non avvengono una volta sola. Ogni modifica successiva, come un elemento aggiunto dal JavaScript, un’immagine che arriva, un font che viene sostituito, può richiedere di ripetere il calcolo degli stili, il layout e il disegno, in tutto o in parte. È così che nascono gli spostamenti misurati dal CLS e il lavoro che rallenta le interazioni misurate dall’INP.
Cosa ritarda il primo disegno
Mettendo insieme i pezzi, il primo disegno di una pagina, misurato dal First Contentful Paint, può avvenire solo quando il browser ha ricevuto l’HTML almeno fino al contenuto da mostrare, ha scaricato ed elaborato tutti i fogli di stile che bloccano il rendering e ha eseguito tutti gli script sincroni che li precedono. Il percorso critico è l’insieme di queste risorse e dei passaggi che le collegano: la sua lunghezza determina quanto l’utente aspetta davanti a uno schermo bianco.
Il percorso critico si misura in tre dimensioni: il numero di risorse critiche, il loro peso complessivo e il numero di viaggi di andata e ritorno necessari per ottenerle. Un foglio di stile che ne importa un altro aggiunge un viaggio; un font dichiarato nel CSS ne aggiunge un altro ancora; uno script che inserisce un foglio di stile ne aggiunge un terzo. Queste catene sono ben visibili nel grafico a cascata delle richieste di rete dei DevTools.
Il caso dei font
I font meritano un discorso a parte, perché si trovano in una posizione particolare del percorso. Il browser scopre di aver bisogno di un font solo quando ha elaborato il CSS e ha trovato un elemento visibile che lo usa: il font parte quindi dopo il foglio di stile, in coda alla catena. Nel frattempo il browser deve decidere cosa fare del testo. Con la regola font-display: block lo nasconde per un breve periodo in attesa del font; con font-display: swap lo mostra subito con un carattere di riserva e lo sostituisce quando il font arriva. La prima scelta ritarda la comparsa del testo, e quindi il primo disegno utile e spesso l’LCP; la seconda mostra subito il contenuto, al prezzo di un piccolo cambiamento visivo che le proprietà di regolazione delle metriche dei font possono rendere quasi impercettibile. Anticipare il font principale con un preload e ospitarlo sul proprio dominio accorcia ulteriormente la catena.
Come accorciare il percorso
Le tecniche per migliorare il primo disegno si capiscono meglio alla luce del percorso critico: ciascuna riduce il numero, il peso o la profondità delle risorse critiche.
- Meno CSS bloccante. Inserire nell’HTML solo gli stili necessari alla parte visibile della pagina e caricare il resto in modo non bloccante, come spiegato nell’articolo sul CSS critico; eliminare le regole inutilizzate.
- Nessuno script sincrono nell’intestazione. Usare
deferper gli script dell’applicazione,asyncper quelli indipendenti, e rimandare fino all’interazione ciò che non serve subito, come nella tecnica del delay del JavaScript. - Catene più corte. Evitare
@importnei fogli di stile, dichiarare le risorse critiche direttamente nell’HTML, anticipare con un preload i font e le immagini che verrebbero scoperti tardi. - HTML che arriva presto. Tutto parte dall’HTML: un TTFB basso, grazie alla cache di pagina, sposta in avanti l’intero percorso.
- Risorse più leggere. Compressione, minificazione e font ridotti ai caratteri necessari, come visto nell’articolo sul subsetting dei font, riducono il tempo di download di ogni risorsa critica.
Dal primo disegno all’LCP
Il percorso critico determina il primo disegno, ma l’LCP dipende anche da un’altra risorsa: l’elemento più grande della pagina, spesso un’immagine. Anche se il CSS è pronto, l’LCP non può avvenire prima che l’immagine principale sia stata scaricata e decodificata. Per questo, all’ottimizzazione del percorso critico va affiancata quella dell’elemento LCP: scoperta precoce nell’HTML, priorità alta, formato e dimensione adeguati. Quando il testo è l’elemento LCP, invece, entra in gioco il font: se il font del titolo arriva tardi e il browser nasconde il testo nell’attesa, l’LCP si sposta di conseguenza.
Come osservarlo
Il pannello Performance dei DevTools di Chrome mostra il percorso critico in azione: la lettura dell’HTML, il calcolo degli stili, il layout, il disegno, con le risorse di rete allineate nel tempo e i momenti del primo disegno e dell’LCP segnati sulla traccia. Nel pannello Network, le risorse che bloccano il rendering si riconoscono per la priorità alta e la posizione all’inizio del caricamento. Lighthouse e PageSpeed Insights riportano un elenco delle risorse che bloccano il rendering e una stima del tempo che si guadagnerebbe eliminandole; nelle versioni più recenti mostrano anche l’albero delle dipendenze di rete, con le catene di richieste critiche.
Un esercizio utile è confrontare la sequenza di fotogrammi di un caricamento con il percorso critico: il momento in cui la pagina smette di essere bianca coincide con l’arrivo dell’ultima risorsa bloccante. Rimuovere quella risorsa dal percorso, o renderla più leggera, sposta visibilmente in avanti il primo fotogramma con contenuto.
Un esempio
Prendiamo la pagina di un articolo su un sito WordPress con un tema ricco di funzioni. Nell’intestazione ci sono cinque fogli di stile, uno dei quali importa i font da un servizio esterno, e tre script sincroni, tra cui una libreria per gli effetti di scorrimento. Il waterfall mostra una catena di quattro passaggi prima che il browser possa disegnare: HTML, foglio di stile principale, foglio di stile importato, file dei font. Da mobile, il primo disegno arriva dopo 2,4 secondi. Gli interventi sono pochi: il CSS necessario alla parte visibile viene inserito nell’HTML e il resto caricato in modo non bloccante, i font vengono ospitati sul sito e dichiarati direttamente, i tre script ricevono defer e la libreria degli effetti viene caricata solo nelle pagine che la usano. Il primo disegno scende sotto il secondo, senza alcuna modifica visibile per chi legge.
In sintesi
Per mostrare una pagina il browser costruisce il DOM dall’HTML e il CSSOM dai fogli di stile, li combina nel render tree, calcola il layout, disegna e compone i livelli. Il CSS nell’intestazione blocca il rendering perché deve essere completo prima di essere applicato; gli script sincroni bloccano la lettura del documento e aspettano il CSS. L’insieme di queste risorse e dei passaggi che le collegano è il percorso critico, e la sua lunghezza determina il momento del primo disegno.
Tutte le principali tecniche di ottimizzazione del caricamento, dal CSS critico agli attributi defer e async, dal preload alla cache di pagina, sono modi di accorciare questo percorso: meno risorse critiche, più leggere, con meno passaggi tra una e l’altra. Capirlo non richiede di diventare sviluppatori, ma permette di leggere un report di prestazioni sapendo perché ogni suggerimento conta, e quale conta di più.