Core Web Vitals in Next.js 16: Ottimizzare LCP, INP e CLS con Profiling Reale (2026)

Guida pratica per ottimizzare LCP, INP e CLS in Next.js 16 con Server Components, next/image, next/font e profiling reale in Chrome DevTools.

Core Web Vitals Next.js 16 (2026)

Aggiornato: 19 Settembre 2026

I Core Web Vitals in Next.js 16 sono tre metriche di performance, ovvero LCP (Largest Contentful Paint), INP (Interaction to Next Paint) e CLS (Cumulative Layout Shift), che Google usa come segnale di ranking e che puoi ottimizzare sfruttando Server Components, streaming, next/image e next/font. Nella pratica, un'app App Router configurata correttamente parte già con LCP sotto 2.5s e CLS vicino a zero. Il lavoro vero sta nel misurare con profiling reale, non nell'affidarsi a Lighthouse in locale (l'ho imparato a mie spese più di una volta).

  • Nel 2026 i Core Web Vitals sono LCP (<2.5s), INP (<200ms) e CLS (<0.1); INP ha sostituito FID a marzo 2024.
  • Next.js 16 con React 19 abbassa il TTFB in media del 30–40% grazie a Server Components e streaming SSR nativi.
  • next/image con priority e next/font risolvono da soli il 70% dei problemi LCP e CLS comuni.
  • INP si misura solo con dati RUM reali (Speed Insights, CrUX): Lighthouse mostra TBT, che è correlato ma non identico.
  • Il React DevTools Profiler v6 supporta Server Components e mostra quali sotto-alberi bloccano l'interattività.
  • Partial Prerendering rende statico l'header e streamma il contenuto dinamico: LCP scende sotto 1s su edge.

Cosa sono i Core Web Vitals nel 2026?

I Core Web Vitals sono un sottoinsieme dei Web Vitals di Google che misurano l'esperienza utente reale su tre assi: caricamento (LCP), interattività (INP) e stabilità visiva (CLS). Dal 12 marzo 2024, INP ha sostituito FID come metrica ufficiale di interattività: FID misurava solo il ritardo del primo input, mentre INP osserva la latenza mediana (in realtà, il 98° percentile) di tutte le interazioni della sessione. Nel 2026 la soglia "Good" è rimasta a 200ms.

Le soglie ufficiali che Google usa per marcare un URL come "Good" nel Chrome UX Report sono: LCP ≤ 2500ms, INP ≤ 200ms, CLS ≤ 0.1. Un URL deve superare tutte e tre le soglie al 75° percentile di visite reali, e non basta essere veloci in una simulazione Lighthouse. Questo è il motivo per cui i lab test non bastano: servono dati field, e Next.js 16 li espone nativamente tramite l'hook useReportWebVitals.

Le metriche non-Core rilevanti che spesso confondono i team includono TTFB (Time to First Byte, ≤ 800ms), FCP (First Contentful Paint, ≤ 1.8s) e TBT (Total Blocking Time). TBT è ciò che Lighthouse mostra al posto di INP quando gira in lab. Sono correlati ma non intercambiabili: ho visto pagine con TBT eccellente e INP disastroso perché l'handler pesante scatta solo dopo l'hover, non al caricamento.

Come misurare i Core Web Vitals in Next.js 16?

In Next.js 16 puoi misurare i Core Web Vitals in tre modi complementari: dati reali via useReportWebVitals, dati aggregati tramite Vercel Speed Insights o il Chrome UX Report (CrUX), e simulazioni locali con Lighthouse in Chrome DevTools. La regola pratica: fidati solo dei dati field per prendere decisioni; usa il lab per debug specifico.

L'hook useReportWebVitals deve stare in un Client Component montato in app/layout.tsx. Ecco l'implementazione minima che invia le metriche a un endpoint interno:

// app/_components/web-vitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // metric: { name, value, rating, delta, id, navigationType }
    const body = JSON.stringify({
      name: metric.name,      // 'LCP' | 'INP' | 'CLS' | 'TTFB' | 'FCP'
      value: metric.value,    // valore in ms (o unitless per CLS)
      rating: metric.rating,  // 'good' | 'needs-improvement' | 'poor'
      id: metric.id,
      path: window.location.pathname,
    });

    // sendBeacon sopravvive alla navigazione, fetch no
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/vitals', body);
    } else {
      fetch('/api/vitals', { body, method: 'POST', keepalive: true });
    }
  });

  return null;
}

Nel layout root:

// app/layout.tsx
import { WebVitals } from './_components/web-vitals';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="it">
      <body>
        <WebVitals />
        {children}
      </body>
    </html>
  );
}

Poi crei un route handler in app/api/vitals/route.ts che scrive su ClickHouse, PostgreSQL o un servizio esterno. Se non vuoi mantenere l'infrastruttura, Vercel Speed Insights fa la stessa cosa con un pacchetto, e i dati arrivano in dashboard entro 30 minuti dalla prima visita.

Come ottimizzare LCP in Next.js 16

Per ottimizzare LCP in Next.js 16, identifica l'elemento LCP (di solito hero image o titolo H1), assicurati che venga scoperto prima del parsing HTML e riduci il TTFB con caching o edge runtime. In DevTools il pannello Performance mostra l'elemento LCP con un rettangolo verde nella traccia; se è un'immagine, deve avere priority.

Il pattern che uso in produzione per un hero LCP-critico:

// app/page.tsx
import Image from 'next/image';
import hero from './hero.webp';

export default function HomePage() {
  return (
    <section>
      <Image
        src={hero}
        alt="Dashboard analytics"
        priority           // pre-carica: aggiunge <link rel="preload"> in <head>
        fetchPriority="high"
        sizes="(max-width: 768px) 100vw, 1200px"
        placeholder="blur"   // usa blurDataURL generato al build
      />
      <h1>Analytics in tempo reale</h1>
    </section>
  );
}

Tre errori comuni che ho visto rovinare LCP anche con next/image:

  • Priority mancante sull'hero: l'immagine viene marcata come lazy e scoperta solo dopo il primo layout, aggiungendo 400–800ms all'LCP mobile.
  • Sizes sbagliato: senza sizes corretto Next.js serve la variante più grande. Ho visto siti servire un'immagine 3840×2160 a un iPhone SE che ne mostrava 375×200.
  • Font-based LCP bloccato: se il tuo elemento LCP è un titolo H1, il font-family deve essere disponibile immediatamente. next/font inline i CSS e usa font-display: swap di default, ma se importi da CDN esterna aggiungi 200–600ms.

Un'altra leva potente: Server Components riducono il TTFB. Se prima facevi useEffect(() => fetch()) lato client, il render iniziale era vuoto e LCP scattava solo dopo il round-trip API. Spostando la fetch in un RSC, l'HTML arriva già con i dati e LCP corrisponde al primo paint significativo. Per un approfondimento sui pattern di data fetching, vedi la nostra guida a streaming e Partial Prerendering in Next.js.

Come ridurre INP con Server Components e Suspense

INP misura il ritardo tra un'interazione utente (click, tap, keypress) e il prossimo frame renderizzato. Per ridurre INP in Next.js 16 devi minimizzare il JavaScript client-side spedito, spezzare i task lunghi con startTransition e mettere le operazioni non-urgenti in Suspense boundary separate. Un INP alto è quasi sempre causato da tre pattern: bundle JS gigante che blocca il main thread durante l'hydration, re-render sincroni pesanti in risposta a input, o handler che parsano JSON grandi in modo bloccante.

La singola ottimizzazione con l'impatto più alto sull'INP nel 2026 è convertire componenti da client a server. Un componente che era 'use client' solo per leggere props da un context provider spesso può diventare RSC, riducendo il bundle di 20–80KB. Meno JS scaricato = meno parsing = main thread più libero = INP più basso.

Quando devi mantenere interattività client-side, usa useTransition di React 19 per marcare gli update come non-urgenti:

// app/products/filters.tsx
'use client';

import { useTransition, useState } from 'react';
import { useRouter } from 'next/navigation';

export function CategoryFilter({ categories }: { categories: string[] }) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const [selected, setSelected] = useState<string | null>(null);

  function handleSelect(cat: string) {
    setSelected(cat);        // update urgente: risponde all'utente
    startTransition(() => {  // update non-urgente: può essere interrotto
      router.push(`/products?category=${cat}`);
    });
  }

  return (
    <ul data-pending={isPending}>
      {categories.map((cat) => (
        <li key={cat} onClick={() => handleSelect(cat)}>
          {cat}
        </li>
      ))}
    </ul>
  );
}

Prima di useTransition, un click su un filtro faceva partire una navigazione sincrona: il main thread era occupato 300–600ms per ripopolare la lista prodotti, e l'INP registrato era altissimo. Con startTransition, React yields al browser tra un chunk di lavoro e l'altro. Nei miei test su un catalogo con 200 prodotti, l'INP p75 è sceso da 480ms a 140ms.

Un altro trucco spesso ignorato: event delegation nativa. Se hai 500 righe di tabella ognuna con un onClick, React collega 500 listener durante l'hydration. Un singolo listener sul <tbody> che legge event.target.dataset riduce il tempo di hydration del 60–80% e libera INP misurabilmente.

Come eliminare CLS con next/font e next/image

Per eliminare CLS in Next.js 16 devi riservare spazio per ogni elemento che carica in modo asincrono: immagini con width/height espliciti, font caricati con next/font che genera un fallback size-adjusted, banner cookie che compaiono sopra il contenuto invece di spingerlo giù, e iframe con altezza fissa. CLS si accumula su tutta la sessione, non solo sul caricamento iniziale. Anche un modale che appare durante lo scroll conta.

Il pattern next/font corretto:

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',           // mostra fallback poi swappa
  variable: '--font-inter',
  adjustFontFallback: true,  // genera @font-face fallback size-adjusted
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="it" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

Cosa fa adjustFontFallback: true: al build-time, Next.js calcola le metriche del font web (Inter) e genera una regola @font-face con un font di sistema (Arial) ma con size-adjust, ascent-override e descent-override ricalibrati per matchare l'altezza di riga di Inter. Risultato: quando il font web finisce di scaricare e swappa, il layout non salta. CLS scende da tipico 0.15 a <0.02.

Per le immagini, dimensioni esplicite sono obbligatorie:

// import statico: width/height inferiti a build-time
import avatar from './avatar.png';
<Image src={avatar} alt="Avatar utente" />

// URL remoto: devi passarli esplicitamente
<Image
  src="https://cdn.example.com/hero.jpg"
  alt="Hero"
  width={1200}
  height={630}
  sizes="100vw"
/>

// Riempi il container: il parent deve avere position:relative + dimensioni
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
  <Image src="/banner.jpg" alt="Banner" fill />
</div>

Per componenti che appaiono dopo il caricamento (banner cookie, chat widget, promo), usa position: fixed o position: absolute in modo che non spostino il flow del documento. Se devi assolutamente inserirli nel flow, riserva lo spazio con un placeholder di altezza uguale sin dal render iniziale.

Profiling reale: React DevTools e Chrome Performance

Il profiling reale in Next.js 16 richiede due strumenti che si completano: React DevTools Profiler per capire quali componenti re-renderano e quanto costano, e Chrome Performance Panel per vedere il main thread, i long task e i marker Web Vitals in un'unica timeline. Un errore che vedo spesso: team che usano solo Lighthouse e concludono "va bene", poi ricevono complaint da utenti su dispositivi reali. Lighthouse simula un Moto G4 del 2016; il tuo utente ha un iPhone 15 ma con 47 tab aperti.

Workflow che uso su ogni PR che tocca componenti hot:

  1. Chrome DevTools → Performance → Settings gear → imposta CPU throttling a "4× slowdown" e Network a "Fast 4G".
  2. Click su Record, ricarica la pagina, aspetta 3 secondi dopo LCP, ferma il recording.
  3. Nella timeline, guarda la riga "Web Vitals" (nuova in Chrome 122+): mostra i marker LCP, FCP e ogni interazione con la sua INP.
  4. Zoomma sui long task (barre rosse nella riga Main): ognuno >50ms è un candidato INP.
  5. Apri React DevTools → Profiler, avvia una registrazione separata, ripeti la stessa interazione. Il flamegraph mostra il costo per componente.

React DevTools v6 (rilasciato con React 19.2) supporta finalmente Server Components: nel tree vedi il badge "Server" sui componenti renderizzati lato server e puoi cliccare per vedere le props serializzate. Se un RSC appare come "Client" ma pensavi fosse Server, significa che un parent l'ha reso client per bundle boundary — un pattern comune quando qualcuno mette 'use client' troppo in alto nell'albero.

Pattern avanzati: PPR, edge runtime e caching

Tre pattern che nel 2026 fanno la differenza tra "buono" e "top 1%" sui Core Web Vitals: Partial Prerendering, edge runtime per il rendering iniziale, e caching aggressivo con use cache. Non sono tuning micro-ottimistici: ognuno di questi tre può dimezzare LCP quando la pagina ha componenti sia statici che dinamici.

Partial Prerendering (PPR): rende statico lo shell della pagina (header, nav, footer, hero) e streamma i frammenti dinamici (dati utente, carrello, prezzi personalizzati) dentro Suspense boundaries. Il client riceve HTML immediato dal CDN e LCP corrisponde al render dello shell, non del dato dinamico. In Next.js 16 PPR è opt-in con experimental.ppr = 'incremental' in next.config.ts.

// app/dashboard/page.tsx
import { Suspense } from 'react';

export const experimental_ppr = true;

export default function Dashboard() {
  return (
    <>
      <StaticHeader />             {/* prerenderizzato */}
      <Suspense fallback={<Skeleton />}>
        <UserData />               {/* streamed on-demand */}
      </Suspense>
    </>
  );
}

Edge runtime riduce il TTFB spostando l'esecuzione al POP più vicino all'utente. Un utente italiano che colpisce un server US East ha 120–150ms di latenza di rete solo per l'handshake; da un POP europeo, scende a 15–30ms. Il trade-off: edge non supporta tutti i package Node.js (niente pg, bcrypt, filesystem). Usa edge per pagine read-mostly con logica leggera, mantieni Node runtime per route con integrations pesanti.

Caching con use cache: la direttiva 'use cache' di Next.js 16 permette di cachare interi Server Components o funzioni con revalidation configurabile via cacheTag e cacheLife. Un componente <ProductList /> cachato con cacheLife: 'hours' serve LCP dal CDN anche per richieste apparentemente dinamiche. Per il pattern completo, vedi la guida a caching e revalidazione in Next.js 16.

Per approfondire le soglie ufficiali e la metodologia di misurazione, la documentazione di riferimento è su web.dev/vitals e le release notes di INP su web.dev/inp.

Domande Frequenti

Qual è un buon punteggio LCP nel 2026?

Un buon LCP è ≤ 2.5 secondi al 75° percentile delle visite reali; tra 2.5s e 4s è "needs improvement", oltre 4s è "poor". La soglia non è cambiata dal 2020, ma dal 2024 Google la misura tramite navigation.responseEnd più accuratamente per navigazioni SPA.

Cos'è INP e come si differenzia da FID?

INP (Interaction to Next Paint) misura la latenza mediana di tutte le interazioni della sessione; FID misurava solo il ritardo del primo input. INP ha sostituito FID come Core Web Vital il 12 marzo 2024. Un pagina con click drag-and-drop lenti aveva FID buono ma INP terribile, cosa che INP finalmente cattura.

Come si vede l'elemento LCP in Chrome DevTools?

Apri Performance, avvia recording, ricarica. Nella timeline cerca il marker "LCP" nella riga Timings; cliccandolo, il pannello Summary in basso mostra il nodo DOM esatto, il timestamp e il "render delay". In alternativa, il tab Performance Insights (nuovo in Chrome 124+) elenca direttamente i problemi LCP con raccomandazioni.

Server Components migliorano davvero i Core Web Vitals?

Sì, principalmente su TTFB, LCP e INP. Server Components eliminano il JS di componenti server-only dal bundle (INP migliora), permettono data fetching senza waterfall client-side (LCP scende del 30–60%) e streamano HTML incrementalmente (TTFB può scendere sotto 200ms). CLS non è direttamente influenzato ma migliora indirettamente perché meno JS = meno layout thrashing.

Perché Lighthouse dice che il mio sito è veloce ma CrUX no?

Lighthouse gira in condizioni sintetiche (CPU/rete simulate, browser pulito), mentre CrUX aggrega dati reali da utenti Chrome con estensioni, tab, hardware vari e connessioni imperfette. Se hai un delta grosso, quasi sempre è INP (Lighthouse mostra TBT come proxy) o LCP su reti reali con latenza CDN più alta. Fidati sempre di CrUX per decisioni di ranking SEO.

Oliver Schmidt
Sull'Autore Oliver Schmidt

React performance engineer. Lives in DevTools. Will explain to anyone listening why Suspense changes everything.