Scopri cos'è lo scanner di precaricamento del browser, in che modo contribuisce alle prestazioni e come puoi evitare di ostacolarlo.
Un aspetto trascurato dell'ottimizzazione della velocità di pagina riguarda la conoscenza di alcuni aspetti interni del browser. I browser apportano alcune ottimizzazioni per migliorare le prestazioni in modi che noi sviluppatori non possiamo fare, ma solo se queste ottimizzazioni non vengono involontariamente ostacolate.
Un'ottimizzazione interna del browser da comprendere è lo scanner di precaricamento del browser. Questo post illustra il funzionamento dello scanner di precaricamento e, soprattutto, come evitare di ostacolarlo.
Che cos'è uno scanner di precaricamento?
Ogni browser ha un parser HTML principale che tokenizza il markup non elaborato e lo elabora in un modello a oggetti. Tutto questo continua allegramente fino a quando il parser non si interrompe quando trova una risorsa di blocco, ad esempio un foglio di stile caricato con un elemento <link> o uno script caricato con un elemento <script> senza un attributo async o defer.
<link> per un file CSS esterno, che impedisce al browser di analizzare il resto del documento, o addirittura di eseguirne il rendering, finché il CSS non viene scaricato e analizzato.
Nel caso dei file CSS, il rendering viene bloccato per evitare un flash di contenuti senza stile (FOUC), ovvero quando una versione senza stile di una pagina può essere visualizzata brevemente prima che vengano applicati gli stili.
Il browser blocca anche l'analisi e il rendering della pagina quando rileva elementi <script> senza un attributo defer o async.
Il motivo è che il browser non può sapere con certezza se un determinato script modificherà il DOM mentre l'analizzatore HTML principale sta ancora svolgendo il suo lavoro. Per questo motivo, è prassi comune caricare il codice JavaScript alla fine del documento, in modo che gli effetti del blocco dell'analisi e del rendering diventino marginali.
Questi sono buoni motivi per cui il browser dovrebbe bloccare sia l'analisi che il rendering. Tuttavia, bloccare uno di questi passaggi importanti non è auspicabile, in quanto può ritardare la scoperta di altre risorse importanti. Fortunatamente, i browser fanno del loro meglio per mitigare questi problemi tramite un parser HTML secondario chiamato scanner di precaricamento.
<body>, ma lo scanner di precaricamento può guardare avanti nel markup non elaborato per trovare la risorsa immagine e iniziare a caricarla prima che l'analizzatore HTML principale venga sbloccato.
Il ruolo di uno scanner di precaricamento è speculativo, ovvero esamina il markup non elaborato per trovare risorse da recuperare in modo opportunistico prima che il parser HTML principale le scopra.
Come capire quando lo scanner di precaricamento è in funzione
Lo scanner di precaricamento esiste a causa del blocco del rendering e dell'analisi. Se questi due problemi di prestazioni non esistessero, lo scanner di precaricamento non sarebbe molto utile. La chiave per capire se una pagina web trae vantaggio dallo scanner di precaricamento dipende da questi fenomeni di blocco. Per farlo, puoi introdurre un ritardo artificiale per le richieste per scoprire dove sta lavorando lo scanner di precaricamento.
Prendi come esempio questa pagina di testo e immagini di base con un foglio di stile. Poiché i file CSS bloccano sia il rendering che l'analisi, introduci un ritardo artificiale di due secondi per il foglio di stile tramite un servizio di proxy. Questo ritardo rende più facile vedere nella sequenza di rete dove sta lavorando lo scanner di precaricamento.
Come puoi vedere nella struttura a cascata, lo scanner di precaricamento rileva l'elemento <img> anche se il rendering e l'analisi del documento sono bloccati. Senza questa ottimizzazione, il browser non può recuperare gli elementi in modo opportunistico durante il periodo di blocco e un numero maggiore di richieste di risorse sarebbe consecutivo anziché simultaneo.
Ora che abbiamo visto questo esempio di base, diamo un'occhiata ad alcuni pattern reali in cui lo scanner di precaricamento può essere aggirato e cosa si può fare per risolvere il problema.
Script async inseriti
Supponiamo che nel tuo <head> sia presente un codice HTML che include un codice JavaScript incorporato come questo:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Gli script inseriti sono async per impostazione predefinita, quindi quando questo script viene inserito, si comporterà come se l'attributo async fosse stato applicato. Ciò significa che verrà eseguito il prima possibile e non bloccherà il rendering. Sembra ottimale, vero? Tuttavia, se presumi che questo <script> in linea venga dopo un elemento <link> che carica un file CSS esterno, otterrai un risultato non ottimale:
async inserito. Lo scanner di precaricamento non può rilevare lo script durante la fase di blocco del rendering, perché viene inserito nel client.
Analizziamo cosa è successo:
- Al secondo 0, viene richiesto il documento principale.
- A 1,4 secondi, arriva il primo byte della richiesta di navigazione.
- A 2,0 secondi, vengono richiesti il CSS e l'immagine.
- Poiché il parser è bloccato durante il caricamento del foglio di stile e il codice JavaScript incorporato che inserisce lo script
asyncviene inserito dopo il foglio di stile a 2,6 secondi, la funzionalità fornita dallo script non è disponibile non appena potrebbe esserlo.
Questo non è ottimale perché la richiesta dello script avviene solo dopo il completamento del download del foglio di stile. In questo modo, lo script non viene eseguito il prima possibile. Al contrario, poiché l'elemento <img> è rilevabile nel markup fornito dal server, viene rilevato dallo scanner di precaricamento.
Cosa succede se utilizzi un tag <script> normale con l'attributo async anziché inserire lo script nel DOM?
<script src="/yall.min.js" async></script>
Questo è il risultato:
async <script>. Lo scanner di precaricamento rileva lo script durante la fase di blocco della visualizzazione e lo carica contemporaneamente al CSS.
Potrebbe esserci la tentazione di suggerire che questi problemi potrebbero essere risolti utilizzando rel=preload. Questo approccio funzionerebbe sicuramente, ma potrebbe comportare alcuni effetti collaterali. Dopotutto, perché utilizzare rel=preload per risolvere un problema che può essere evitato non inserendo un elemento <script> nel DOM?
async inserito, ma lo script async viene precaricato per garantire che venga rilevato prima.
Il precaricamento "risolve" il problema, ma ne introduce uno nuovo: lo script async nelle prime due demo, nonostante sia caricato in <head>, viene caricato con priorità "Bassa", mentre il foglio di stile viene caricato con priorità "Massima". Nell'ultima demo in cui lo script async è precaricato, il foglio di stile viene comunque caricato con la priorità"Più alta", ma la priorità dello script è stata promossa ad "Alta".
Quando la priorità di una risorsa viene aumentata, il browser le assegna più larghezza di banda. Ciò significa che, anche se il foglio di stile ha la priorità più alta, la priorità elevata dello script potrebbe causare una contesa della larghezza di banda. Questo potrebbe essere un fattore in caso di connessioni lente o di risorse di grandi dimensioni.
La risposta è semplice: se uno script è necessario durante l'avvio, non vanificare l'azione dello scanner di precaricamento inserendolo nel DOM. Sperimenta in base alle tue esigenze con il posizionamento dell'elemento <script>, nonché con attributi come defer e async.
Caricamento lento con JavaScript
Il caricamento lento è un ottimo metodo per risparmiare dati, spesso applicato alle immagini. Tuttavia, a volte il caricamento lento viene applicato in modo errato alle immagini che si trovano "above the fold", per così dire.
Ciò introduce potenziali problemi di rilevabilità delle risorse per quanto riguarda lo scanner di precaricamento e può ritardare inutilmente il tempo necessario per scoprire un riferimento a un'immagine, scaricarla, decodificarla e presentarla. Prendiamo ad esempio questo markup dell'immagine:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
L'utilizzo di un prefisso data- è un pattern comune nei lazy loader basati su JavaScript. Quando l'immagine viene visualizzata nella sezione visibile, il caricatore lento rimuove il prefisso data-, il che significa che nell'esempio precedente data-src diventa src. Questo aggiornamento richiede al browser di recuperare la risorsa.
Questo pattern non è problematico finché non viene applicato alle immagini che si trovano nell'area visibile durante l'avvio. Poiché lo scanner di precaricamento non legge l'attributo data-src nello stesso modo in cui leggerebbe un attributo src (o srcset), il riferimento all'immagine non viene rilevato prima. Inoltre, il caricamento dell'immagine viene ritardato fino a dopo il download, la compilazione e l'esecuzione del codice JavaScript del caricamento lento.
A seconda delle dimensioni dell'immagine, che possono dipendere dalle dimensioni dell'area visibile, potrebbe essere un elemento candidato per la metrica Largest Contentful Paint (LCP). Quando lo scanner di precaricamento non riesce a recuperare in modo speculativo la risorsa immagine in anticipo, ad esempio nel momento in cui i fogli di stile della pagina bloccano il rendering, l'LCP ne risente.
La soluzione è modificare il markup dell'immagine:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Questo è il pattern ottimale per le immagini che si trovano nell'area visibile durante l'avvio, poiché lo scanner di precaricamento rileverà e recupererà la risorsa immagine più rapidamente.
Il risultato in questo esempio semplificato è un miglioramento di 100 millisecondi della metrica LCP su una connessione lenta. Potrebbe non sembrare un grande miglioramento, ma lo è se consideri che la soluzione è una rapida correzione del markup e che la maggior parte delle pagine web è più complessa di questo insieme di esempi. Ciò significa che i candidati LCP potrebbero dover competere per la larghezza di banda con molte altre risorse, quindi ottimizzazioni come questa diventano sempre più importanti.
Immagini di sfondo CSS
Ricorda che lo scanner di precaricamento del browser analizza il markup. Non analizza altri tipi di risorse, come CSS, che potrebbero comportare il recupero di immagini a cui fa riferimento la proprietà background-image.
Come per l'HTML, i browser elaborano il CSS nel proprio modello a oggetti, noto come CSSOM. Se durante la creazione del CSSOM vengono rilevate risorse esterne, queste vengono richieste al momento del rilevamento e non dallo scanner di precaricamento.
Supponiamo che il candidato LCP della tua pagina sia un elemento con una proprietà CSS background-image. Ecco cosa succede durante il caricamento delle risorse:
background-image (riga 3). Il recupero dell'immagine richiesta non inizia finché l'analizzatore CSS non la trova.
In questo caso, lo scanner di precaricamento non viene tanto sconfitto quanto non coinvolto. Tuttavia, se un candidato LCP nella pagina proviene da una proprietà CSS background-image, ti consigliamo di precaricare l'immagine:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Questo suggerimento rel=preload è piccolo, ma aiuta il browser a scoprire l'immagine prima di quanto farebbe altrimenti:
background-image (riga 3). Il suggerimento rel=preload aiuta il browser a scoprire l'immagine circa 250 millisecondi prima rispetto a quando non viene utilizzato.
Con il suggerimento rel=preload, l'elemento candidato LCP viene rilevato prima, riducendo il tempo LCP. Sebbene questo suggerimento aiuti a risolvere il problema, l'opzione migliore potrebbe essere quella di valutare se l'immagine candidata LCP deve essere caricata dal CSS. Con un tag <img>, avrai un maggiore controllo sul caricamento di un'immagine appropriata per l'area visibile, consentendo al contempo allo scanner di precaricamento di individuarla.
Inlining di troppe risorse
L'inlining è una pratica che consiste nell'inserire una risorsa all'interno del codice HTML. Puoi incorporare fogli di stile negli elementi <style>, script negli elementi <script> e praticamente qualsiasi altra risorsa utilizzando la codifica Base64.
L'incorporamento delle risorse può essere più veloce del download perché non viene inviata una richiesta separata per la risorsa. È direttamente nel documento e si carica all'istante. Tuttavia, esistono svantaggi significativi:
- Se non memorizzi nella cache il codice HTML, e non puoi farlo se la risposta HTML è dinamica, le risorse incorporate non vengono mai memorizzate nella cache. Ciò influisce sulle prestazioni perché le risorse incorporate non sono riutilizzabili.
- Anche se puoi memorizzare nella cache l'HTML, le risorse incorporate non vengono condivise tra i documenti. Ciò riduce l'efficienza della memorizzazione nella cache rispetto ai file esterni che possono essere memorizzati nella cache e riutilizzati in un'intera origine.
- Se inserisci troppi elementi in linea, ritardi la scoperta delle risorse da parte dello scanner di precaricamento più avanti nel documento, perché il download di questi contenuti aggiuntivi in linea richiede più tempo.
Prendiamo come esempio questa pagina. In determinate condizioni, il candidato LCP è l'immagine nella parte superiore della pagina e il CSS si trova in un file separato caricato da un elemento <link>. La pagina utilizza anche quattro caratteri web che vengono richiesti come file separati dalla risorsa CSS.
<img>, ma viene rilevato dallo scanner di precaricamento perché il CSS e i caratteri richiesti per il caricamento pagina vengono caricati in risorse separate, il che non impedisce allo scanner di precaricamento di svolgere il suo lavoro.
Cosa succede se il CSS e tutti i caratteri sono incorporati come risorse base64?
<img>, ma l'incorporamento del CSS e delle sue quattro risorse dei caratteri nel `` impedisce allo scanner di precaricamento di rilevare l'immagine finché queste risorse non vengono scaricate completamente.
In questo esempio, l'impatto dell'inlining ha conseguenze negative per LCP e per le prestazioni in generale. La versione della pagina che non incorpora nulla mostra l'immagine LCP in circa 3,5 secondi. La pagina che incorpora tutto non mostra l'immagine LCP fino a poco più di 7 secondi.
In questo caso, non si tratta solo dello scanner di precaricamento. L'incorporamento dei caratteri non è una buona strategia perché base64 è un formato inefficiente per le risorse binarie. Un altro fattore in gioco è che le risorse dei caratteri esterni non vengono scaricate, a meno che non siano ritenute necessarie dal CSSOM. Quando questi caratteri sono incorporati come base64, vengono scaricati indipendentemente dal fatto che siano necessari o meno per la pagina corrente.
Un precaricamento potrebbe migliorare la situazione? Certo. Potresti precaricare l'immagine LCP e ridurre il tempo LCP, ma l'aggiunta di risorse in linea al tuo HTML potenzialmente non memorizzabile nella cache ha altre conseguenze negative sulle prestazioni. Anche il First Contentful Paint (FCP) è influenzato da questo pattern. Nella versione della pagina in cui non è presente alcun elemento in linea, il valore FCP è di circa 2,7 secondi. Nella versione in cui tutto è in linea, l'FCP è di circa 5,8 secondi.
Fai molta attenzione quando inserisci elementi in linea nell'HTML, in particolare le risorse con codifica in base64. In genere non è consigliabile, ad eccezione di risorse molto piccole. Utilizza l'incorporamento il meno possibile, perché un utilizzo eccessivo è come giocare con il fuoco.
Rendering del markup con JavaScript lato client
Non c'è dubbio: JavaScript influisce sicuramente sulla velocità della pagina. Non solo gli sviluppatori dipendono da questo linguaggio per fornire interattività, ma c'è anche la tendenza a fare affidamento su di esso per fornire i contenuti stessi. Questo approccio migliora l'esperienza degli sviluppatori in alcuni modi, ma i vantaggi per gli sviluppatori non sempre si traducono in vantaggi per gli utenti.
Un pattern che può vanificare l'azione dello scanner di precaricamento è il rendering del markup con JavaScript lato client:
Quando i payload di markup sono contenuti e visualizzati interamente da JavaScript nel browser, tutte le risorse in quel markup sono effettivamente invisibili allo scanner di precaricamento. Questo ritarda il rilevamento di risorse importanti, il che influisce sicuramente sull'LCP. Nel caso di questi esempi, la richiesta dell'immagine LCP è significativamente ritardata rispetto all'esperienza equivalente con rendering lato server che non richiede la visualizzazione di JavaScript.
Questo argomento si discosta un po' dal focus di questo articolo, ma gli effetti del rendering del markup sul client vanno ben oltre l'annullamento dello scanner di precaricamento. Innanzitutto, l'introduzione di JavaScript per un'esperienza che non lo richiede introduce un tempo di elaborazione non necessario che può influire su Interaction to Next Paint (INP). Il rendering di quantità estremamente elevate di markup sul client ha maggiori probabilità di generare attività lunghe rispetto alla stessa quantità di markup inviata dal server. Il motivo, oltre all'elaborazione aggiuntiva richiesta da JavaScript, è che i browser trasmettono il markup dal server e suddividono il rendering in modo da limitare le attività lunghe. Il markup sottoposto a rendering dal client, invece, viene gestito come un'unica attività monolitica, il che potrebbe influire sull'INP di una pagina.
La soluzione per questo scenario dipende dalla risposta a questa domanda: esiste un motivo per cui il markup della pagina non può essere fornito dal server anziché essere visualizzato sul client? Se la risposta è "no", è consigliabile prendere in considerazione il rendering lato server (SSR) o il markup generato staticamente, se possibile, in quanto aiuterà lo scanner di precaricamento a scoprire e recuperare in modo opportunistico le risorse importanti in anticipo.
Se la tua pagina ha bisogno di JavaScript per collegare la funzionalità ad alcune parti del markup della pagina, puoi comunque farlo con l'SSR, utilizzando JavaScript vanilla o l'idratazione per ottenere il meglio da entrambi i mondi.
Aiuta lo scanner di precaricamento ad aiutarti
Lo scanner di precaricamento è un'ottimizzazione del browser molto efficace che aiuta le pagine a caricarsi più velocemente durante l'avvio. Evitando pattern che impediscono di scoprire in anticipo le risorse importanti, non solo semplifichi lo sviluppo, ma crei anche esperienze utente migliori che produrranno risultati migliori in molte metriche, tra cui alcuni Web Vitals.
Per riassumere, ecco i punti chiave di questo post:
- Lo scanner di precaricamento del browser è un parser HTML secondario che esegue la scansione prima di quello principale se quest'ultimo è bloccato, per scoprire in modo opportunistico le risorse che può recuperare prima.
- Le risorse che non sono presenti nel markup fornito dal server nella richiesta di navigazione iniziale non possono essere rilevate dallo scanner di precaricamento. I modi in cui lo scanner di precaricamento può essere aggirato possono includere (ma non sono limitati a):
- Inserimento di risorse nel DOM con JavaScript, che si tratti di script, immagini, fogli di stile o qualsiasi altro elemento che sarebbe meglio inserire nel payload di markup iniziale dal server.
- Caricamento lento di immagini o iframe above the fold utilizzando una soluzione JavaScript.
- Eseguire il rendering del markup sul client che potrebbe contenere riferimenti alle risorse secondarie del documento utilizzando JavaScript.
- Lo scanner di precaricamento analizza solo l'HTML. Non esamina i contenuti di altre risorse, in particolare CSS, che potrebbero includere riferimenti ad asset importanti, inclusi i candidati LCP.
Se, per qualsiasi motivo, non puoi evitare un pattern che influisce negativamente sulla capacità dello scanner di precaricamento di velocizzare le prestazioni di caricamento, prendi in considerazione l'indicazione della risorsa rel=preload. Se utilizzi rel=preload, esegui dei test negli strumenti di laboratorio per assicurarti di ottenere l'effetto desiderato. Infine, non precaricare troppe risorse, perché se dai la priorità a tutto, non la darai a niente.