Analisi delle prestazioni del percorso di rendering critico

Ilya Grigorik
Ilya Grigorik

Data di pubblicazione: 31 marzo 2014

L'identificazione e la risoluzione dei colli di bottiglia critici del percorso di rendering richiedono una buona conoscenza delle insidie comuni. Un tour guidato per identificare i modelli di rendimento comuni ti aiuterà a ottimizzare le tue pagine.

L'ottimizzazione del percorso di rendering critico consente al browser di visualizzare la pagina il più rapidamente possibile: pagine più veloci si traducono in un maggiore coinvolgimento, un maggior numero di pagine visualizzate e una conversione migliorata. Per ridurre al minimo il tempo che un visitatore trascorre a visualizzare una schermata vuota, dobbiamo ottimizzare le risorse da caricare e l'ordine in cui vengono caricate.

Per illustrare questa procedura, inizia con il caso più semplice possibile e crea gradualmente la pagina in modo da includere risorse, stili e logica di applicazione aggiuntivi. Durante la procedura, ottimizzeremo ogni caso e vedremo anche dove possono verificarsi errori.

Finora ci siamo concentrati esclusivamente su ciò che accade nel browser dopo che la risorsa (file CSS, JS o HTML) è disponibile per l'elaborazione. Abbiamo ignorato il tempo necessario per recuperare la risorsa dalla cache o dalla rete. Supponiamo che:

  • Un round trip di rete (latenza di propagazione) al server costa 100 ms.
  • Il tempo di risposta del server è di 100 ms per il documento HTML e di 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>

Prova

Inizia con un markup HTML di base e una singola immagine, senza CSS o JavaScript. Quindi, apri il riquadro Rete in Chrome DevTools e controlla la struttura a cascata delle risorse risultante:

CRP

Come previsto, il download del file HTML ha richiesto circa 200 ms. Tieni presente che la parte trasparente della linea blu rappresenta il periodo di tempo durante il quale il browser attende sulla rete senza ricevere byte di risposta, mentre la parte solida indica il tempo necessario per completare il download dopo aver ricevuto i primi byte di risposta. Il download del codice HTML è molto piccolo (<4K), quindi è sufficiente un solo viaggio di andata e ritorno per recuperare il file completo. Di conseguenza, il recupero del documento HTML richiede circa 200 ms, con metà del tempo trascorso in attesa sulla rete e l'altra metà in attesa della risposta del server.

Quando i contenuti HTML diventano disponibili, il browser analizza i byte, li converte in token e crea la struttura DOM. Tieni presente che DevTools riporta comodamente il tempo dell'evento DOMContentLoaded in basso (216 ms), che corrisponde anche alla linea verticale blu. L'intervallo tra la fine del download dell'HTML e la linea verticale blu (DOMContentLoaded) è il tempo necessario al browser per creare la struttura DOM, in questo caso solo pochi millisecondi.

Nota che la nostra "foto incredibile" non ha bloccato l'evento domContentLoaded. In realtà, possiamo costruire la struttura di rendering e persino dipingere la pagina senza attendere ogni risorsa presente al suo interno: non tutte le risorse sono fondamentali per ottenere una prima visualizzazione rapida. Infatti, quando parliamo del percorso di rendering critico, in genere ci riferiamo al markup HTML, al CSS e a JavaScript. Le immagini non bloccano il rendering iniziale della pagina, anche se dovremmo anche provare a visualizzarle il prima possibile.

Detto questo, l'evento load (noto anche come onload) è bloccato sull'immagine: DevTools segnala l'evento onload dopo 335 ms. Ricorda che l'evento onload indica il punto in cui tutte le risorse richieste dalla pagina sono state scaricate ed elaborate; a questo punto, la rotellina di caricamento può interrompere la rotazione nel browser (la linea verticale rossa nella struttura a cascata).

Aggiunta di JavaScript e CSS

La nostra pagina "Esperienza "Hello World" sembra di base, ma sotto il cofano c'è molto di più. In pratica, avremo bisogno di più del codice HTML: è probabile che avremo un foglio di stile CSS e uno o più script per aggiungere interattività alla nostra pagina. Aggiungi entrambi per vedere 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>

Prova

Prima di aggiungere JavaScript e CSS:

CRP DOM

Con JavaScript e CSS:

DOM, CSSOM, JS

L'aggiunta di file CSS e JavaScript esterni aggiunge due richieste aggiuntive alla nostra struttura a cascata, tutte inviate dal browser all'incirca nello stesso momento. Tuttavia, tieni presente che ora la differenza di tempo tra gli eventi domContentLoaded e onload è più ridotta.

Che cosa è successo?

  • A differenza del nostro esempio HTML semplice, dobbiamo anche recuperare e analizzare il file CSS per costruire il CSSOM e abbiamo bisogno sia del DOM che del CSSOM per costruire la struttura di rendering.
  • Poiché la pagina contiene anche un file JavaScript che blocca il parser, l'evento domContentLoaded viene bloccato fino a quando il file CSS non viene scaricato e analizzato: poiché JavaScript potrebbe eseguire query sul CSSOM, dobbiamo bloccare il file CSS fino al suo download prima di poter eseguire JavaScript.

Cosa succede se sostituiamo lo script esterno con uno script in linea? Anche se lo script è inserito direttamente nella pagina, il browser non può eseguirlo finché non viene creato il CSSOM. In breve, anche il codice JavaScript in linea blocca il parser.

Detto questo, nonostante il blocco su CSS, l'incorporamento dello script velocizza il rendering della pagina? Prova e vedi cosa succede.

JavaScript esterno:

DOM, CSSOM, JS

JavaScript in linea:

DOM, CSSOM e JS in linea

Stiamo inviando una richiesta in meno, ma i tempi di onload e domContentLoaded sono praticamente gli stessi. Perché? Sappiamo che non importa se il codice JavaScript è in linea o esterno, perché non appena il browser raggiunge il tag script, si blocca e attende la compilazione del CSSOM. Inoltre, nel nostro primo esempio, il browser scarica sia CSS che JavaScript in parallelo e il download termina all'incirca nello stesso momento. In questo caso, l'inserimento in linea del codice JavaScript non ci aiuta molto. Esistono però diverse strategie che possono velocizzare il rendering della pagina.

Innanzitutto, ricorda che tutti gli script in linea bloccano il parser, ma per gli script esterni possiamo aggiungere l'attributo async per sbloccare il parser. Annulla la digitazione e prova:

<!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>

Prova

JavaScript (esterno) che blocca l'interprete:

DOM, CSSOM, JS

JavaScript asincrono (esterno):

DOM, CSSOM, JS asincrono

Molto meglio. L'evento domContentLoaded viene attivato poco dopo l'analisi dell'HTML; il browser sa di non bloccare JavaScript e, poiché non sono presenti altri script di blocco dell'interprete, anche la costruzione del CSSOM può procedere in parallelo.

In alternativa, avremmo potuto inserire in linea sia il CSS sia il 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>

Prova

DOM, CSS in linea, JS in linea

Tieni presente che il tempo domContentLoaded è praticamente lo stesso dell'esempio precedente; anziché contrassegnare il codice JavaScript come asincrono, abbiamo inserito in linea sia il CSS sia il codice JavaScript nella pagina stessa. Questo rende la pagina HTML molto più grande, ma il vantaggio è che il browser non deve attendere per recuperare le risorse esterne; tutto è già 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 quali risorse sono "critiche" e scegliere tra diverse strategie su come includerle nella pagina. Non esiste una soluzione unica a questo problema, perché ogni pagina è diversa. Occorre seguire autonomamente una procedura simile per individuare la strategia ottimale.

Detto questo, vediamo se possiamo fare un passo indietro e identificare alcuni schemi generali di rendimento.

Pattern di rendimento

La pagina più semplice possibile consiste unicamente nel markup HTML: non CSS, JavaScript o altri tipi di risorse. Per visualizzare questa pagina, il browser deve avviare la richiesta, attendere l'arrivo del documento HTML, analizzarlo, generare 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>

Prova

CRP Hello World

Il tempo tra T0 e T1 acquisisce i tempi di elaborazione della rete e del server. Nel migliore dei casi (se il file HTML è di piccole dimensioni), l'intero documento viene recuperato con un solo viaggio di andata e ritorno sulla rete. A causa del funzionamento dei protocolli di trasporto TCP, i file di dimensioni maggiori possono richiedere più viaggi di andata e ritorno. Di conseguenza, nel migliore dei casi la pagina sopra indicata ha un percorso di rendering critico di andata e ritorno (minimo).

Consideriamo ora 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>

Prova

DOM + CSSOM CRP

Ancora una volta, si verifica un viaggio di andata e ritorno sulla rete per recuperare il documento HTML e 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 di poter visualizzare la pagina sullo schermo. Di conseguenza, questa pagina richiede almeno due round trip prima di poter essere visualizzata. Ancora una volta, il file CSS potrebbe richiedere più roundtrip, da qui l'enfasi su "minimo".

Di seguito sono riportati alcuni termini che utilizziamo per descrivere il percorso di rendering critico:

  • Risorsa critica: risorsa che potrebbe bloccare il rendering iniziale della pagina.
  • Lunghezza del percorso critico: numero di viaggi di andata e ritorno o tempo totale necessario per recuperare tutte le risorse critiche.
  • 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 una singola pagina HTML, conteneva una singola risorsa critica (il documento HTML); la lunghezza del percorso critico era uguale anche a un viaggio di andata e ritorno della rete (supponendo che il file fosse di piccole dimensioni) e i byte critici totali erano solo le dimensioni di trasferimento del documento HTML stesso.

Ora confrontalo con le caratteristiche del percorso critico del precedente esempio HTML e CSS:

DOM + CSSOM CRP

  • 2 risorse critiche
  • 2 o più viaggi di andata e ritorno per la lunghezza minima del percorso critico
  • 9 KB di byte critici

Abbiamo bisogno sia dell'HTML che del CSS per costruire la struttura di rendering. Di conseguenza, sia HTML che CSS sono risorse critiche: il CSS viene recuperato solo dopo che il browser riceve il documento HTML, quindi la lunghezza del percorso critico è di almeno due roundtrip. Entrambe le risorse sommano fino a 9 kB di byte critici.

Ora aggiungi al mix un altro file JavaScript.

<!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>

Prova

Abbiamo aggiunto app.js, che è sia un asset JavaScript esterno sulla pagina sia una risorsa di blocco del parser (ovvero critica). Peggio ancora, per eseguire il file JavaScript dobbiamo bloccare e attendere il CSSOM; ricorda che JavaScript può eseguire query sul CSSOM e quindi il browser si mette in pausa fino al download di style.css e alla creazione del CSSOM.

DOM, CSSOM, CRP JavaScript

Detto questo, in pratica, se esaminiamo la "cascata di rete" di questa pagina, vedrai che le richieste CSS e JavaScript vengono avviate all'incirca nello stesso momento; il browser riceve il codice HTML, rileva entrambe le risorse e avvia entrambe le richieste. Di conseguenza, la pagina mostrata nell'immagine precedente presenta le seguenti caratteristiche del percorso critico:

  • 3 risorse fondamentali
  • 2 o più viaggi di andata e ritorno per la lunghezza minima del percorso critico
  • 11 KB di byte critici

Ora abbiamo tre risorse critiche che aggiungono fino a 11 KB di byte critici, ma la lunghezza del percorso critico è ancora di due roundtrip perché possiamo trasferire il codice CSS e JavaScript in parallelo. Capire le caratteristiche del percorso di rendering critico significa essere in grado di identificare le risorse critiche e anche di capire in che modo il browser ne programmerà il recupero.

Dopo aver chattato con gli sviluppatori del nostro sito, ci siamo resi conto che il codice JavaScript che abbiamo incluso nella nostra pagina non deve essere bloccato; abbiamo alcuni dati e altro codice che non devono bloccare il rendering della nostra pagina. Con queste informazioni, possiamo aggiungere l'attributo async all'elemento <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>

Prova

DOM, CSSOM, CRP JavaScript asincrono

Uno script asincrono presenta diversi vantaggi:

  • Lo script non blocca più il parser e non fa parte del percorso di rendering critico.
  • Poiché non sono presenti altri script critici, il CSS non deve bloccare l'evento domContentLoaded.
  • Prima viene attivato l'evento domContentLoaded, prima può iniziare l'esecuzione dell'altra logica di applicazione.

Di conseguenza, la nostra pagina ottimizzata ora torna a due risorse critiche (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, come sarebbe?

<!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>

Prova

CRP per DOM, CSS non bloccanti e JavaScript asincrono

Poiché la risorsa style.css viene utilizzata solo per la stampa, il browser non deve bloccarsi per visualizzare la pagina. Pertanto, non appena la costruzione del DOM è completata, il browser dispone di informazioni sufficienti per eseguire il rendering della pagina. Di conseguenza, questa pagina ha una sola risorsa critica (il documento HTML) e la lunghezza minima del percorso di rendering critico è un viaggio di andata e ritorno.

Feedback