INP (Interaction to Next Paint) misura il ritardo tra qualsiasi interazione dell'utente (clic, tap, pressione tasto) e il momento in cui il browser aggiorna visivamente la pagina. Un INP ≤200ms è "Good", 200-500ms è "Needs Improvement", >500ms è "Poor". Per ottimizzare INP bisogna ridurre le long task sul main thread, minimizzare l'input delay con code deferral, e ottimizzare il presentation delay. Nei dati CrUX globali, circa il 12% delle pagine web ha INP classificato "Poor" su mobile.
Perché Google ha sostituito FID con INP
FID (First Input Delay) misurava solo la latenza della prima interazione nella vita di una pagina: utile ma incompleto. Un sito poteva avere FID eccellente e comunque risultare lento e "sticky" dopo il caricamento iniziale, quando l'utente naviga i menu, apre modale, e interagisce con form. INP misura il percentile 98 di tutte le interazioni durante l'intera visita: nessuna interazione lenta sfugge. Da marzo 2024 è la metrica ufficiale nel set Core Web Vitals.
Il cambiamento da FID a INP ha sorpreso molti team di sviluppo: siti con FID "Good" di 50ms hanno scoperto di avere INP "Poor" di 600ms. La ragione è che FID misurava solo il ritardo della prima interazione (spesso un semplice click su un link, con pochissimo codice JS associato), mentre INP cattura le interazioni successive quando la pagina è carica di stato: form con validazione, filtri di ricerca, menu con animazioni, e altri componenti interattivi che accumulano lavoro sul main thread.
Le tre componenti di INP
INP si scompone in tre fasi: Input Delay (il tempo che passa dal click all'inizio dell'event handler, spesso causato da long task che occupano il main thread), Processing Time (il tempo di esecuzione del codice JavaScript associato all'interazione), e Presentation Delay (il tempo che il browser impiega a renderizzare il frame dopo che il JS ha terminato). Ognuna ha soluzioni diverse.
Input Delay alto (>50ms): il main thread è bloccato da altre long task quando l'utente clicca. Soluzione: ridurre le long task al caricamento, usare `scheduler.yield()` per spezzare il lavoro, e spostare operazioni pesanti in Web Workers. Processing Time alto (>100ms): il codice dell'event handler stesso è lento. Soluzione: ottimizzare la logica JS dell'handler, memoizzare i calcoli costosi, e evitare DOM manipulation sincrone estese. Presentation Delay alto (>50ms): il browser impiega molto a renderizzare dopo il JS. Soluzione: ridurre il numero di elementi DOM, evitare layout thrashing, e usare `will-change` con parsimonia.
Long Tasks: il nemico principale dell'INP
Una "long task" è qualsiasi attività sul main thread che supera i 50ms. Il browser non può rispondere agli input durante una long task: se l'utente clicca mentre il thread è bloccato da una long task di 300ms, quel ritardo si somma all'INP. Le cause più comuni: esecuzione di script di terze parti al caricamento, elaborazione di grandi dataset in loop sincroni, re-render costosi di componenti React non ottimizzati, e idratazione lenta su app SSR.
Un pattern particolarmente insidioso: la React hydration su pagine SSR con molto stato. Next.js genera HTML statico server-side (ottimo per LCP), ma l'idratazione client-side che "attiva" il JavaScript può richiedere 200-800ms su dispositivi mid-range durante i quali il main thread è occupato. L'utente vede la pagina (LCP ottimo) ma non può interagire (INP pessimo). La soluzione: Partial Hydration (isole) o React Server Components che riducono il JavaScript da idratare.
Come identificare le long task: Performance panel
Apri Chrome DevTools → tab Performance → registra mentre interagisci con il sito. Le long task appaiono come barre con angolo rosso nel main thread. Clicca su una long task per vedere la call stack: scopri quale funzione è responsabile e quanto tempo impiega. L'estensione Web Vitals mostra l'INP in tempo reale mentre navighi, rendendo più facile riprodurre interazioni problematiche.
Una tecnica che usiamo negli audit INP: registra il Performance panel per 30 secondi di navigazione normale (apri menu, clicca link, usa filtri di ricerca, compila form). Poi filtra per "Long Tasks" nella visualizzazione. Ogni long task sopra 100ms va esaminata: guarda la call stack per identificare il codice responsabile. Spesso la causa è una singola funzione che potrebbe essere ottimizzata o spostata fuori dal main thread in 1-2 ore di lavoro.
Spezzare le long task con scheduler e yielding
La tecnica fondamentale è "yielding": interrompere periodicamente il lavoro pesante per dare al browser la possibilità di gestire gli input. In pratica si usa `setTimeout(fn, 0)` o la più moderna `scheduler.yield()` (disponibile in Chrome) per cedere il controllo al browser tra chunk di lavoro. In React, `startTransition` segna gli aggiornamenti di stato come non urgenti, permettendo al browser di gestire gli input prima di elaborarli.
Un esempio concreto di yielding: hai un filtro di ricerca che deve processare 10.000 items. Invece di processarli in un loop sincrono (una long task da 500ms), processali in chunks di 200 con `setTimeout(processNextChunk, 0)` tra un chunk e l'altro. Il browser può gestire i click dell'utente tra un chunk e l'altro, mantenendo l'INP basso. Il processamento totale richiederà leggermente più tempo (millisecondi in più), ma la reattività percepita migliorerà drasticamente.
Ottimizzare i gestori di eventi
I gestori di eventi (click, keydown, touchstart) non devono fare lavoro pesante in modo sincrono. Regola pratica: ogni gestore dovrebbe completare in meno di 50ms. Lavoro pesante va spostato in `requestIdleCallback` (per task non urgenti), Web Workers (per computazioni CPU-intensive in background), o spezzato in chunk con `requestAnimationFrame`. Nelle nostre realizzazioni di siti web analizziamo sempre l'INP su mobile prima del go-live.
Un caso comune nei siti e-commerce: il click sul bottone "Aggiungi al carrello" che sincronamente aggiorna il DOM, ricalcola totali, aggiorna il contatore nel header, e fa una chiamata API — tutto in un singolo event handler. Questo può facilmente richiedere 300-500ms. La refactoring corretta: aggiorna immediatamente l'UI (ottimistic update) con il minimo DOM necessario, poi in modo asincrono completa le operazioni secondarie. L'utente percepisce una risposta immediata al click.
React specifico: ridurre i re-render costosi
In React ogni clic che aggiorna lo stato causa un re-render. Se il componente che si aggiorna ha molti figli o calcoli pesanti nel render, questo può diventare una long task. Soluzioni: `React.memo` per memoizzare componenti che non dipendono dallo stato che cambia, `useMemo` per calcoli costosi, `useCallback` per stabilizzare le funzioni passate come prop, e `virtualization` (react-virtual o @tanstack/virtual) per liste con migliaia di elementi.
Il React DevTools Profiler è lo strumento giusto per identificare i re-render costosi: mostra ogni componente, quante volte ha fatto re-render durante una sessione, e quanto tempo ha impiegato ogni render. I "wasted renders" (componenti che rendono con gli stessi props e producono lo stesso output) sono i candidati per `React.memo`. I componenti con render time superiore a 16ms (1 frame a 60fps) sono priorità assoluta per l'ottimizzazione.
Misurare l'INP nei dati reali
Il tuo INP in Lighthouse è spesso ottimistico: l'emulatore non riproduce le interazioni reali. Usa la libreria `web-vitals` di Google per misurare l'INP degli utenti reali e inviarlo al tuo analytics. Google Search Console mostra l'INP aggregato per URL. Vercel Analytics con Web Vitals mostra LCP, INP e CLS per ogni pagina in produzione.
Integrare la misurazione INP in produzione richiede poche righe di codice: `import { onINP } from "web-vitals"; onINP((metric) => { sendToAnalytics({ name: metric.name, value: metric.value, rating: metric.rating }); });`. Questo invia ogni misura INP al tuo sistema di analytics (GA4, Mixpanel, o endpoint personalizzato). Con qualche settimana di dati, puoi identificare quali pagine e quali interazioni specifiche hanno INP alto — informazione molto più preziosa del punteggio Lighthouse aggregato. La gestione dei siti web che offriamo include setup e monitoraggio INP per tutti i siti che seguiamo.




