L'identificazione e la risoluzione dei colli di bottiglia dei percorsi di rendering critici richiede una buona conoscenza degli inconvenienti più comuni. Facciamo un tour pratico per scoprire i modelli di rendimento più comuni che ti aiuteranno a ottimizzare le tue pagine.
L'ottimizzazione del percorso di rendering critico consente al browser di visualizzare la pagina il più rapidamente possibile: le pagine più veloci si traducono in un maggiore coinvolgimento, in un maggior numero di pagine visualizzate e in un miglioramento delle conversioni. Per ridurre al minimo il tempo che un visitatore trascorre visualizzando una schermata vuota, dobbiamo ottimizzare quali risorse vengono caricate e in quale ordine.
Per illustrare questo processo, iniziamo con il caso più semplice possibile e creiamo in modo incrementale la nostra pagina per includere ulteriori risorse, stili e logica dell'applicazione. Durante la procedura, ottimizzeremo ogni caso e vedremo dove possono verificarsi errori.
Finora ci siamo concentrati esclusivamente su ciò che accade nel browser una volta che la risorsa (CSS, JS o file HTML) è disponibile per l'elaborazione. Abbiamo ignorato il tempo necessario per recuperare la risorsa dalla cache o dalla rete. Supponiamo che:
- Un roundtrip di rete (latenza di propagazione) al server costa 100 ms.
- Tempo di risposta del server: 100 ms per il documento HTML e 10 ms per tutti gli altri file.
L'esperienza Hello World
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Inizieremo con il markup HTML di base e una singola immagine, senza CSS o JavaScript. Apriamo la sequenza temporale della rete in Chrome DevTools e analizziamo la struttura a cascata delle risorse risultante:
Come previsto, il download del file HTML ha richiesto circa 200 ms. Tieni presente che la parte trasparente della riga blu rappresenta il tempo di attesa del browser sulla rete senza ricevere byte di risposta, mentre la parte piena indica il tempo necessario per terminare il download dopo la ricezione dei primi byte di risposta. Il download HTML ha dimensioni minime (<4K), quindi tutto ciò di cui abbiamo bisogno è un singolo roundtrip per recuperare l'intero file. Di conseguenza, il recupero del documento HTML richiede circa 200 ms, di cui metà del tempo di attesa sulla rete e l'altra metà di attesa della risposta del server.
Quando i contenuti HTML diventano disponibili, il browser analizza i byte, li converte in token e crea l'albero DOM. Tieni presente che DevTools segnala comodamente l'ora dell'evento DOMContentLoaded in basso (216 ms), che corrisponde anche alla linea verticale blu. Il divario tra la fine del download del codice HTML e la riga verticale blu (DOMContentLoaded) è il tempo impiegato dal browser per creare l'albero DOM, in questo caso pochi millisecondi.
Nota che la nostra "foto meravigliosa" non ha bloccato l'evento domContentLoaded
. Abbiamo scoperto che possiamo creare l'albero di rendering e persino colorare la pagina senza attendere ogni asset al suo interno: non tutte le risorse sono fondamentali per eseguire rapidamente la prima visualizzazione. Infatti, quando parliamo di percorso di rendering critico, in genere parliamo di markup HTML, CSS e JavaScript. Le immagini non bloccano il rendering iniziale della pagina, anche se dovremmo anche provare a fare in modo che le immagini vengano visualizzate il prima possibile.
Detto questo, l'evento load
(noto anche come onload
) è bloccato sull'immagine: DevTools segnala l'evento onload
a 335 ms. Ricorda che l'evento onload
segna il momento in cui tutte le risorse richieste dalla pagina sono state scaricate ed elaborate; a questo punto, la rotellina di caricamento può smettere di ruotare nel browser (la linea verticale rossa nella struttura a cascata).
Aggiunta di JavaScript e CSS alla combinazione
La nostra pagina "Hello World experience" sembra semplice, ma c'è molto da fare. In pratica, avremo bisogno di qualcosa di più del semplice codice HTML: è probabile che avremo un foglio di stile CSS e uno o più script per aggiungere interattività alla nostra pagina. Aggiungiamoli entrambi e vediamo cosa succede:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="timing.js"></script>
</body>
</html>
Prima di aggiungere JavaScript e CSS:
Con JavaScript e CSS:
L'aggiunta di file CSS e JavaScript esterni aggiunge altre due richieste alla struttura a cascata, che vengono inviate dal browser quasi contemporaneamente. Tuttavia, tieni presente che ora c'è una differenza di tempo molto minore tra gli eventi domContentLoaded
e onload
.
Che cosa è successo?
- A differenza del nostro esempio in HTML semplice, dobbiamo anche recuperare e analizzare il file CSS per creare il CSSOM, oltre al DOM e al CSSOM per creare l'albero di rendering.
- Poiché la pagina contiene anche un file JavaScript di blocco dell'analizzatore sintattico, l'evento
domContentLoaded
viene bloccato finché il file CSS non viene scaricato e analizzato. Poiché JavaScript potrebbe eseguire query sul CSSOM, dobbiamo bloccare il file CSS finché non viene scaricato prima di poter eseguire JavaScript.
Cosa succede se sostituiamo il nostro script esterno con uno incorporato? Anche se lo script è incorporato direttamente nella pagina, il browser non può eseguirlo fino a quando non viene creato il CSSOM. In breve, anche il codice JavaScript incorporato è il blocco dell'analizzatore sintattico.
Detto questo, a prescindere dal blocco su CSS, l'incorporamento dello script velocizza il rendering della pagina? Proviamo e vediamo cosa succede.
JavaScript esterno:
JavaScript incorporato:
Stiamo facendo una richiesta in meno, ma gli orari di onload
e domContentLoaded
sono effettivamente gli stessi. Perché? Sappiamo che il fatto che JavaScript sia in linea o esterno non è importante, in quanto non appena il browser tocca il tag script, lo blocca e attende la creazione del CSSOM. Inoltre, nel nostro primo esempio, il browser scarica sia CSS che JavaScript in parallelo e il download termina quasi contemporaneamente. In questo caso, l'inserimento del codice JavaScript non è di grande aiuto. Esistono però diverse strategie che possono velocizzare il rendering della pagina.
Innanzitutto, ricorda che tutti gli script incorporati bloccano l'analizzatore sintattico, ma per gli script esterni possiamo aggiungere la parola chiave "asinc" per sbloccare quest'ultimo. Annullamo l'operazione incorporata e proviamo:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Async</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script async src="timing.js"></script>
</body>
</html>
JavaScript di blocco dell'analisi (esterno):
JavaScript (esterno) asincrono:
Molto meglio! L'evento domContentLoaded
si attiva poco dopo l'analisi dell'HTML; il browser non riesce a bloccare JavaScript e, poiché non sono presenti altri script di blocco dell'analizzatore sintattico, la creazione del CSSOM può procedere in parallelo.
In alternativa, avremmo potuto incorporare sia il codice CSS che JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Inlined</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
Tieni presente che l'ora domContentLoaded
è sostanzialmente la stessa dell'esempio precedente; invece di contrassegnare il codice JavaScript come asincrono, abbiamo incorporato sia CSS che JS nella pagina stessa. In questo modo la nostra pagina HTML è molto più grande, ma l'aspetto positivo è che il browser non deve aspettare per recuperare eventuali risorse esterne; tutto è esattamente lì nella pagina.
Come puoi vedere, anche con una pagina molto semplice, l'ottimizzazione del percorso di rendering critico è un esercizio non banale: dobbiamo comprendere il grafico delle dipendenze tra le diverse risorse, identificare le risorse "critiche" e scegliere tra diverse strategie per includerle nella pagina. Non esiste una soluzione unica a questo problema, ogni pagina è diversa. Devi seguire autonomamente una procedura simile per individuare la strategia ottimale.
Detto ciò, vediamo se possiamo fare un passo indietro e identificare alcuni pattern generali di rendimento.
Pattern di rendimento
La pagina più semplice possibile consiste semplicemente nel markup HTML: nessun CSS, nessun codice JavaScript o altri tipi di risorse. Per eseguire il rendering di questa pagina, il browser deve avviare la richiesta, attendere l'arrivo del documento HTML, analizzarlo, creare il DOM e infine eseguire il rendering sullo schermo:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Il tempo tra T0 e T1 determina i tempi di elaborazione della rete e del server. Nel migliore dei casi (se il file HTML è di piccole dimensioni), un solo round trip di rete recupera l'intero documento. A causa del funzionamento dei protocolli di trasporto TCP, i file più grandi potrebbero richiedere più round trip. Di conseguenza, nella migliore delle ipotesi la pagina precedente ha un percorso di rendering critico di andata e ritorno (minimo).
Ora consideriamo la stessa pagina ma con un file CSS esterno:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Ancora una volta, dobbiamo effettuare un roundtrip di rete per recuperare il documento HTML e quindi il markup recuperato ci dice che abbiamo bisogno anche del file CSS. Ciò significa che il browser deve tornare al server e recuperare il CSS prima che possa visualizzare la pagina sullo schermo. Di conseguenza, questa pagina comporta almeno due round trip prima di poter essere visualizzata. Anche in questo caso, il file CSS può richiedere più round trip, da qui l'enfasi sul "minimo".
Definiamo il vocabolario che utilizziamo per descrivere il percorso di rendering critico:
- Risorsa critica: risorsa che potrebbe bloccare il rendering iniziale della pagina.
- Lunghezza critica del percorso: numero di round trip o tempo totale necessario per recuperare tutte le risorse fondamentali.
- Byte critici:numero totale di byte necessari per arrivare al primo rendering della pagina, ovvero la somma delle dimensioni dei file di trasferimento di tutte le risorse critiche. Il nostro primo esempio, con un'unica pagina HTML, conteneva un'unica risorsa critica (il documento HTML); la lunghezza del percorso critico corrispondeva inoltre a un round trip di rete (supponendo che il file fosse di piccole dimensioni) e i byte critici totali corrispondevano solo alla dimensione di trasferimento del documento HTML stesso.
Ora confrontiamola con le caratteristiche fondamentali del percorso dell'esempio HTML + CSS sopra riportato:
- 2 risorse critiche
- 2 o più round trip per la lunghezza critica minima del percorso critico
- 9 kB di byte critici
Per creare l'albero di rendering, abbiamo bisogno sia di HTML che di CSS. Di conseguenza, sia HTML che CSS sono risorse fondamentali: il CSS viene recuperato solo dopo che il browser ottiene il documento HTML, quindi la lunghezza del percorso critico è di almeno due round trip. Entrambe le risorse hanno una somma totale di 9 kB di byte critici.
Ora aggiungiamo un altro file JavaScript al mix.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
Abbiamo aggiunto app.js
, che è sia un asset JavaScript esterno nella pagina sia una risorsa di blocco dell'analizzatore sintattico (ovvero fondamentale). Peggio ancora, per eseguire il file JavaScript dobbiamo bloccare il CSSOM e attendere il CSSOM; ricorda che JavaScript può eseguire una query sul CSSOM e quindi il browser si ferma fino a quando style.css
non viene scaricato e creato il CSSOM.
Detto questo, in pratica, se esaminiamo la "struttura a cascata della rete" di questa pagina, noterai che entrambe le richieste CSS e JavaScript vengono avviate all'incirca nello stesso momento; il browser ottiene il codice HTML, rileva entrambe le risorse e avvia entrambe le richieste. Di conseguenza, la pagina precedente presenta le seguenti caratteristiche del percorso critico:
- 3 risorse critiche
- 2 o più round trip per la lunghezza critica minima del percorso critico
- 11 kB di byte critici
Ora abbiamo tre risorse critiche che sommano fino a 11 kB di byte critici, ma la lunghezza del percorso critico è sempre di due round trip perché possiamo trasferire CSS e JavaScript in parallelo. Capire le caratteristiche di un percorso di rendering critico significa poter identificare le risorse critiche e comprendere anche in che modo il browser pianifica i recuperi. Continuiamo con il nostro esempio.
Dopo aver chattato con i nostri sviluppatori del sito, ci rendiamo conto che il codice JavaScript che abbiamo incluso nella nostra pagina non deve essere bloccato; al suo interno sono presenti alcuni dati analitici e altro codice che non deve bloccare il rendering della pagina. Sulla base di queste informazioni, possiamo aggiungere l'attributo "async" al tag script per sbloccare il parser:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Uno script asincrono offre diversi vantaggi:
- Lo script non è più il blocco dell'analizzatore sintattico e non fa parte del percorso di rendering critico.
- Poiché non sono presenti altri script critici, non è necessario che il CSS blocchi l'evento
domContentLoaded
. - Prima viene attivato l'evento
domContentLoaded
, prima potrà iniziare l'esecuzione dell'altra logica dell'applicazione.
Di conseguenza, la pagina ottimizzata torna a due risorse fondamentali (HTML e CSS), con una lunghezza minima del percorso critico di due round trip e un totale di 9 kB di byte critici.
Infine, se il foglio di stile CSS fosse necessario solo per la stampa, quale sarebbe l'aspetto?
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" media="print" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Poiché la risorsa style.css viene utilizzata solo per la stampa, il browser non deve bloccarla per eseguire il rendering della pagina. Pertanto, non appena la creazione del DOM è completa, il browser dispone di informazioni sufficienti per visualizzare la pagina. Di conseguenza, questa pagina ha una sola risorsa critica (il documento HTML) e la lunghezza minima del percorso di rendering critico è di un roundtrip.