Diego Betto's Blog
Foto di Marc-Olivier Jodoin su Unsplash

28 agosto 2026 · 6 min di lettura

Core Web Vitals: LCP, CLS e INP spiegati con un caso reale

Cosa sono davvero LCP, CLS e INP, perché il tuo Lighthouse locale spesso mente, e i fix reali applicati su questo blog per uscirne.

Condividi:XLinkedInFacebookWhatsApp

Qualche settimana fa ho lanciato Lighthouse su questo stesso blog aspettandomi un 100 pieno in Performance. È uscito 95. Total Blocking Time a 182ms, un report pieno di JavaScript “non usato” che non avevo mai scritto io, e un errore in console causato — testuale — da un’estensione del browser che analizza motori di ricerca. Nessuna di quelle tre cose aveva a che fare con il codice del sito.

Questo articolo è quello che ho imparato a distinguere da allora: cosa misurano davvero LCP, CLS e INP, quando il numero che vedi è rumore invece che un problema reale, e i tre fix genuini che ho applicato qui — con i numeri, non solo l’affermazione.

Le tre metriche, senza il gergo

LCP (Largest Contentful Paint) misura quanto ci mette a comparire l’elemento più grande visibile nel viewport iniziale — quasi sempre un’immagine hero o un blocco di testo grande. Sotto 2.5 secondi è “buono”.

CLS (Cumulative Layout Shift) misura quanto la pagina “salta” mentre si carica — un’immagine senza width/height che spinge giù il testo sotto è l’esempio da manuale. Sotto 0.1 è “buono”.

INP (Interaction to Next Paint) misura il tempo tra un click/tap dell’utente e il momento in cui il browser aggiorna visivamente lo schermo in risposta. Sotto 200ms è “buono”.

ℹ Nota

INP ha sostituito FID nel marzo 2024: se hai letto guide più vecchie, probabilmente parlano di FID (First Input Delay) come terza metrica. Google l’ha ufficialmente sostituita con INP perché FID misurava solo il ritardo prima che l’interazione iniziasse a essere processata — INP misura l’intera esperienza, fino al frame renderizzato. Un sito può avere FID ottimo e INP pessimo se l’interazione parte subito ma il lavoro che ne consegue blocca il thread principale a lungo.

Il problema che (quasi) nessuna guida ti dice: il tuo Lighthouse locale mente

Ecco cosa ho trovato scavando nel JSON grezzo di quel report da 95: decine di voci chrome-extension:// negli audit su JavaScript inutilizzato. Un password manager. React DevTools. Un blocker di annunci con i suoi filtri EasyList/EasyPrivacy caricati. Tutti eseguiti durante la misurazione, tutti conteggiati nel Total Blocking Time come se fossero codice del sito.

L’errore in console che faceva fallire l’audit Best Practices? net::ERR_BLOCKED_BY_CLIENT su uno script di analytics di Cloudflare — bloccato dal mio stesso ad blocker, non un bug del sito.

⚠ Attenzione

Non fidarti di un audit Lighthouse lanciato da un profilo Chrome normale: se hai estensioni installate — e chi sviluppa ne ha sempre almeno cinque — quello che Lighthouse misura include il loro codice in esecuzione sulla pagina. Per un numero attendibile: usa una finestra in incognito con le estensioni disabilitate, oppure PageSpeed Insights, che gira in un ambiente controllato lontano dal tuo browser.

C’è anche una distinzione più profonda da tenere a mente, che va oltre le estensioni: i dati di laboratorio non sono i dati di campo. Lighthouse (lab data) esegue un’unica pagina caricata una volta, in condizioni simulate. Il Core Web Vitals reale che Google usa per il ranking viene dal CrUX (Chrome UX Report) — dati aggregati da utenti veri, su una finestra mobile di 28 giorni, visibili in Search Console. Un sito può avere un Lighthouse locale perfetto e un INP di campo mediocre se, per dire, la maggior parte del traffico reale arriva da smartphone di fascia bassa che il tuo laptop da sviluppo non simula.

Tre fix reali, applicati qui

Tolto il rumore, restavano tre problemi genuinamente miei. Li riporto con il prima/dopo perché la teoria senza numeri non convince nessuno.

1. Un CSS che bloccava il render

Il foglio di stile globale del sito (~10KB compresso, condiviso da ogni pagina) era una richiesta render-blocking: il browser doveva scaricarlo e parsarlo prima del primo paint. Con Astro, la soluzione è stata una riga di configurazione:

// astro.config.mjs
export default defineConfig({
  build: {
    inlineStylesheets: 'always',
  },
});

Con inlineStylesheets: 'always', il CSS finisce direttamente dentro l’<head> di ogni pagina invece che in un file esterno da richiedere. Il compromesso è reale — perdi la cache condivisa del file tra una pagina e l’altra — ma per un blog dove la maggior parte dei visitatori arriva da una ricerca su un singolo articolo (non naviga tra dieci pagine nella stessa sessione), eliminare quella richiesta bloccante conta più della cache.

2. L’immagine LCP caricata come se non contasse

Le prime tre card della homepage — quelle sempre visibili senza scroll — usavano loading="lazy" sulla cover, la stessa direttiva usata per tutte le immagini più in basso nella pagina. Per l’elemento che è l’LCP, il lazy loading è controproducente: dice al browser di aspettare prima ancora di iniziare a scoprire quella risorsa.

<Image
  src={cover}
  alt={coverAlt}
  loading={isAboveTheFold ? 'eager' : 'lazy'}
  fetchpriority={isFirstCard ? 'high' : undefined}
  width={400}
  height={400}
/>

loading="eager" sulle card sopra la piega, e fetchpriority="high" solo sulla primissima — quella con più probabilità statistica di essere l’elemento LCP effettivo. Non serve dare priorità massima a tutto: solo a quella.

3. Immagini più pesanti di quanto servisse

Le stesse card servivano un raster fisso a 400×400px indipendentemente da quanto fossero effettivamente grandi a schermo — su certi breakpoint del layout a griglia, venivano mostrate a 250×250px reali. Byte scaricati per pixel mai mostrati.

<Image
  src={cover}
  widths={[260, 400, 600]}
  sizes="(min-width: 768px) 260px, (min-width: 640px) 45vw, calc(100vw - 2rem)"
  ...
/>

Con widths + sizes, Astro genera un vero srcset e il browser sceglie la variante giusta in base alla larghezza reale del layout, non alla più grande disponibile a prescindere.

💬 Opinione personale

Rincorrere il 100 esatto è quasi sempre tempo sprecato: dopo questi tre fix, i margini rimasti erano frazioni di punto — FCP e LCP già sotto il secondo, a un pelo dal punteggio massimo per via della curva di scoring di Lighthouse, non per un problema reale da risolvere. A un certo punto conviene fermarsi e guardare i dati di campo reali (Search Console → Core Web Vitals) invece di ottimizzare all’infinito un numero di laboratorio che i tuoi utenti non vedranno mai.

Cosa portarsi a casa

Se stai leggendo un report Lighthouse basso e non sai da dove iniziare: separa prima il rumore (estensioni, script di terze parti bloccati dal tuo stesso browser) dal segnale, poi guarda specificamente l’elemento LCP (sta caricando lazy? ha fetchpriority?) e le immagini sopra la piega (sono dimensionate per il layout reale o solo per il caso peggiore?). Sono i due punti in cui, empiricamente, si nasconde la maggior parte dei problemi risolvibili in un pomeriggio.

Se il tuo INP di campo resta alto anche dopo aver sistemato LCP e CLS, il sospetto successivo è JavaScript che blocca il thread principale su interazioni frequenti — ne ho scritto separatamente parlando di come debounce e throttle possano evitare che un singolo evento (scroll, resize, digitazione) scateni lavoro ripetuto e non necessario. E se usi Astro, vale la pena controllare anche piccole cose come la gestione dei trailing slash, che a prima vista sembrano dettagli di routing ma possono generare redirect evitabili — un altro paio di millisecondi persi prima ancora che la pagina inizi a caricare davvero.

Condividi:XLinkedInFacebookWhatsApp