Come ho ridotto il mio LCP del 70%
Scopri i metodi avanzati per migliorare i Core Web Vitals

Migliorare le metriche LCP con Web Workers e caricamento immagini in 2 fasi
La maggior parte delle volte, un elemento immagine di grandi dimensioni nel viewport visibile diventerà l'elemento Largest Contentful Paint. Anche dopo aver applicato tutte le best practice di Lighthouse come il ridimensionamento delle immagini, la compressione delle immagini, la conversione in WebP e il preloading dell'elemento LCP, il tuo Largest Contentful Paint potrebbe comunque non superare i Core Web Vitals.
L'unico modo per risolvere questo problema è utilizzare tattiche più avanzate come il caricamento in 2 fasi e il threading della pagina con i Web Workers per liberare risorse sul main thread.

Ultima revisione di Arjen Karel a marzo 2026
Un po' di contesto
Sono un esperto di pagespeed e il mio sito web è la mia vetrina. Sulla mia homepage affermo con orgoglio che il mio sito è il più veloce al mondo. Ecco perché ho bisogno che la mia pagina si carichi il più velocemente possibile e di spremere ogni goccia di pagespeed dal mio sito.
Le tecniche che ti mostrerò oggi potrebbero non essere fattibili per un normale sito (WordPress) senza il supporto di un team di sviluppo dedicato e di talento. Se non puoi duplicare questa tecnica sul tuo sito, ti incoraggio comunque a leggere l'articolo e a imparare come penso alla pagespeed e quali sono le mie considerazioni.
Il problema: immagini di grandi dimensioni nel viewport visibile
Un'immagine di grandi dimensioni nel viewport visibile diventerà spesso l'elemento Largest Contentful Paint. Accade spesso che questa immagine LCP non superi i Core Web Vitals. Vedo risultati come questo quotidianamente.

Esistono diversi modi per assicurarsi che questo elemento appaia velocemente sullo schermo:
- Precaricare l'elemento LCP. Il preloading dell'immagine LCP assicurerà che questa immagine sia disponibile per il browser il prima possibile. Combina questo con
fetchpriority="high"per indicare al browser di dare priorità a questa immagine rispetto ad altre risorse. - Usa immagini responsive. Assicurati di non servire immagini di dimensioni desktop ai dispositivi mobili.
- Comprimi le tue immagini. La compressione delle immagini potrebbe ridurre drasticamente le dimensioni dell'immagine.
- Usa formati immagine di nuova generazione. I formati immagine di nuova generazione come WebP superano i formati più vecchi come JPEG e PNG in quasi tutti i casi.
- Minimizzare il critical rendering path. Elimina tutte le risorse che bloccano la renderizzazione come JavaScript e fogli di stile che potrebbero ritardare l'LCP.
Sfortunatamente, nonostante tutte queste ottimizzazioni, in alcuni casi le metriche LCP potrebbero ancora non superare l'audit dei Core Web Vitals. Perché? La sola dimensione dell'immagine è sufficiente a ritardare la fase di resource load duration dell'LCP.
La soluzione: caricamento in 2 fasi e Web Workers
La soluzione che ho implementato (dopo aver ottimizzato tutti gli altri problemi sul mio sito) è il caricamento delle immagini in 2 fasi.
L'idea è semplice: al primo render mostra un'immagine di bassa qualità con le stesse esatte dimensioni dell'immagine finale di alta qualità. Immediatamente dopo la visualizzazione di quell'immagine, avvia il processo che scambia l'immagine di bassa qualità con un'immagine di alta qualità.
Un'implementazione molto semplice potrebbe essere simile a questa: per prima cosa aggiungi un event listener di caricamento a un'immagine. Quando l'immagine viene caricata, lo stesso event listener si scollega e l'src dell'immagine viene scambiato con l'immagine finale di alta qualità.
<img
width="100"
height="100"
alt="testo alt"
src="lq.webp"
onload="this.onload=null;this.src='hq.webp'"
>
Fase 1: webp di bassa qualità 3-5kb

Fase 2: webp di alta qualità 20-40kb

Questo potrebbe sembrare abbastanza semplice (e lo è), ma scambiare un gran numero di immagini all'inizio del processo di rendering causerà troppa attività sul main thread e influenzerà altre metriche dei Core Web Vitals.
Ecco perché ho scelto di delegare parte del lavoro a un Web Worker. Un Web Worker viene eseguito in un nuovo thread e non ha un vero accesso alla pagina corrente. La comunicazione tra il Web Worker e la pagina avviene tramite un sistema di messaggistica. Il vantaggio ovvio è che non stiamo utilizzando il main thread della pagina stessa; stiamo liberando risorse lì. Lo svantaggio è che l'utilizzo di un Web Worker può essere un po' macchinoso.
Il processo in sé non è così difficile. Una volta attivato l'evento DOMContentLoaded, raccolgo tutte le immagini nella pagina. Se un'immagine è stata caricata, la scambierò immediatamente. Se non è stata caricata (perché l'immagine potrebbe essere caricata in lazy load), collegherò un event listener che scambia l'immagine dopo il lazy load.
Un avvertimento importante: il browser tratta ogni swap di immagine come un nuovo candidato LCP. Se lo swap dell'immagine di alta qualità avviene dopo 2,5 secondi, l'LCP verrà misurato al momento dello swap, non al momento del placeholder. Ecco perché è importante che il Web Worker recuperi e scambi l'immagine il più velocemente possibile.
Il risultato: spettacolare.

Il codice per il caricamento LCP in 2 fasi tramite un Web Worker
Ecco il codice che utilizzo per velocizzare il mio LCP tramite il caricamento in 2 fasi e un Web Worker. Il codice sulla pagina principale chiama un Web Worker che recupererà le immagini. Il Web Worker passa il risultato come blob alla pagina principale. Ricevuto il blob, l'immagine viene scambiata.
Worker.js
self.addEventListener('message', async event => {
const newimageURL = event.data.src.replace("/lq-","/resize-");
const response = await fetch(newimageURL)
const blob = await response.blob()
// Invia i dati dell'immagine al thread UI!
self.postMessage({
uid: event.data.uid,
blob: blob,
})
})
Script.js
Lo script.js verrà eseguito come un normale script sulla pagina web attiva. Lo script carica prima il worker. Quindi scorrerà tutte le immagini in una pagina. Ciò accade all'inizio del processo di rendering. Un'immagine potrebbe essere già caricata oppure no. Se un'immagine di bassa qualità è già caricata, chiamerà immediatamente il processo di swap. Se non è ancora caricata, collegherà un listener all'evento di caricamento dell'immagine che avvia il processo di swap non appena l'immagine viene caricata.
Quando un'immagine viene caricata, viene generato un ID univoco per quell'immagine. Ciò mi consente di ritrovare facilmente l'immagine nella pagina (ricorda, il worker non ha accesso al DOM, quindi non posso inviare il nodo DOM dell'immagine). L'URL dell'immagine e l'ID univoco vengono quindi inviati al worker. Quando il worker ha recuperato l'immagine, questa viene rimandata allo script come blob. Lo script infine scambia il vecchio URL dell'immagine con l'URL del blob creato dal Web Worker.
var myWorker = new Worker('/path-to/worker.js');
// invia un messaggio al worker
const sendMessage = (img) => {
// uid rende più facile trovare l'immagine
var uid = create_UID();
// imposta data-uid sull'elemento immagine
img.dataset.uid = uid;
// invia messaggio al worker
myWorker.postMessage({ src: img.src, uid: uid });
};
// genera l'uid
const create_UID = () => {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (new Date().getTime() + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uid;
}
// quando riceviamo un risultato dal worker
myWorker.addEventListener('message', event => {
// Prendi i dati del messaggio dall'evento
const imageData = event.data
// Ottieni l'elemento originale per questa immagine
const imageElement = document.querySelectorAll("img[data-uid='" + imageData.uid + "']");
// Possiamo usare il `Blob` come sorgente dell'immagine! Dobbiamo solo convertirlo
// prima in un URL oggetto
const objectURL = URL.createObjectURL(imageData.blob)
// Una volta caricata l'immagine, vorremo fare un po' di pulizia extra
imageElement.onload = () => {
URL.revokeObjectURL(objectURL)
}
imageElement[0].setAttribute('src', objectURL)
})
// prendi tutte le immagini
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('img[loading="lazy"]').forEach(
img => {
// l'immagine è già visibile?
img.complete ?
// swap immediato
sendMessage(img) :
// swap al caricamento
img.addEventListener(
"load", i => { sendMessage(img) }, { once: true }
)
})
})
Per verificare il miglioramento dell'LCP sul campo, utilizza Real User Monitoring per monitorare l'esperienza dei tuoi visitatori reali sulla pagina. Gli strumenti di laboratorio come Lighthouse mostreranno il miglioramento, ma i dati sul campo di utenti reali su connessioni variabili sono ciò che conta per superare i Core Web Vitals.
17 years of fixing PageSpeed.
I have optimized platforms for some of the largest publishers and e-commerce sites in Europe. I provide the strategy, the code, and the RUM verification. Usually in 1 to 2 sprints.
View Services
