Hai mai cliccato su un pulsante e aspettato mezzo secondo prima che succedesse qualcosa? O peggio: hai toccato un link su mobile e lo schermo è rimasto congelato per un secondo? Quella sensazione di lentezza non è solo fastidiosa — è un problema tecnico che si chiama INP (Interaction to Next Paint), una delle tre metriche dei Core Web Vitals. E se il tuo sito ha un INP alto, Google lo penalizza nei ranking. Molti sviluppatori lo ignorano finché non vedono un crollo nelle performance. Noi, di Meteora Web, abbiamo visto decine di siti rallentare proprio per colpa di JavaScript bloccante e un main thread intasato. In questa guida ti spieghiamo perché succede e come risolverlo, con codice reale e azioni concrete.
Cos’è l’INP e Perché il Main Thread è il Collo di Bottiglia
INP misura il tempo che intercorre tra l’interazione dell’utente (click, tap, pressione di un tasto) e il momento in cui il browser è in grado di dipingere il frame successivo. In pratica: quanto velocemente il sito risponde. Google considera buono un INP inferiore a 200 ms, e soglia di attenzione tra 200 e 500 ms. Oltre i 500 ms è fallimento.
Il colpevole principale è il main thread. Il browser esegue HTML, CSS, JavaScript, calcoli di layout e pittura su un unico thread. Se il tuo JavaScript è troppo pesante o male organizzato, blocca tutto. Il risultato? L’utente clicca, ma il browser è occupato a elaborare codice o a fare reflow, e non può rispondere.
Esempio concreto real-life: Un nostro cliente aveva un e-commerce con un carrello che, al click su “Aggiungi”, eseguiva una serie di chiamate API, aggiornamenti DOM e calcoli di sconto. Il tutto in un unico lungo task JavaScript. L’INP su mobile era oltre i 700 ms. Abbiamo spostato le operazioni non critiche in requestIdleCallback e frammentato il resto in micro-task. L’INP è sceso a 180 ms. Il cliente non ha cambiato nient’altro e le conversioni sono cresciute del 12%.
Sponsored Protocol
Come Diagnosticare l’INP: Strumenti e Metriche
Prima di ottimizzare, devi misurare. Non basta usare PageSpeed Insights (che dà una media). Devi guardare il comportamento reale degli utenti. Noi usiamo tre strumenti:
- Chrome DevTools > Performance: registro un flusso, identifico i long task (>50 ms) che bloccano il main thread.
- Web Vitals Library: attacchiamo il listener
onINP()per catturare dati reali dal campo (RUM). - Search Console > Core Web Vitals: per vedere se Google segnala problemi di INP su URL specifiche.
Una volta trovato un long task, guardi nella timeline: “Task” con durata >50 ms. Clicchi sopra e vedi cosa lo causa: una funzione, un loop, un event listener pesante.
Ottimizzare il Main Thread: 3 Tecniche Fondamentali
Ecco cosa facciamo noi quando il main thread è troppo occupato. Ogni tecnica ha il suo perché.
1. Suddividere i Long Task con yield e setTimeout
Un long task blocca il main thread per più di 50 ms. Se hai un ciclo che processa 1000 elementi, il browser non può rispondere ai click durante quel ciclo. La soluzione è spezzare il lavoro in chunk e lasciare spazio al browser di gestire le interazioni tra un chunk e l’altro.
function processLargeArray(items) {
let index = 0;
const chunkSize = 50;
function processChunk() {
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// elabora items[i]
}
index = end;
if (index < items.length) {
// Cede il controllo al browser prima del prossimo chunk
setTimeout(processChunk, 0);
}
}
processChunk();
}
Perché funziona: setTimeout(fn, 0) non esegue subito la callback, ma la mette in coda. Nel frattempo il browser può gestire eventi utente (click, scroll). Sembra magia, ma è scheduling di base.
Sponsored Protocol
2. Delegare il Lavoro a un Web Worker
I Web Worker eseguono script in un thread separato. Non bloccano il main thread. Ideali per calcoli pesanti (elaborazione immagini, parsing JSON, crittografia) o per operazioni periodiche che non servono istantaneamente.
// worker.js
self.addEventListener('message', (e) => {
const data = e.data;
const result = heavyCalculation(data);
self.postMessage(result);
});
function heavyCalculation(data) {
// simulazione di lavoro pesante
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += Math.sqrt(i);
}
return sum;
}
// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
console.log('Risultato dal worker:', e.data);
};
Attenzione: i worker non hanno accesso al DOM. Non puoi aggiornare la UI dentro un worker. Però puoi far eseguire tutto il calcolo fuori dal main thread e poi ricevere il risultato per aggiornare il DOM velocemente.
3. Ridurre il Lavoro di Event Listener e Reflow
Un errore comune: attachare event listener su ogni elemento di una lista, o peggio, su elementi che si spostano dinamicamente. Ogni listener consuma risorse e può innescare reflow (quando modifichi dimensioni o posizioni).
Sponsored Protocol
Soluzione: usa delegazione eventi (un unico listener sul contenitore) e batched DOM updates.
// Invece di 100 listener individuali:
document.querySelectorAll('.item').forEach(el => {
el.addEventListener('click', handleClick);
});
// Usa un unico listener sul contenitore:
const container = document.getElementById('list-container');
container.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
handleClick(item);
}
});
// Per batch: raccogli le modifiche e applica in un colpo solo
function updateUI(items) {
// Leggi le proprietà una volta (evitano reflow forzati)
const heights = items.map(el => el.getBoundingClientRect().height);
// Poi scrivi in batch
requestAnimationFrame(() => {
items.forEach((el, i) => {
el.style.height = (heights[i] * 2) + 'px';
});
});
}
Errori da evitare: leggere e scrivere miscele nel loop (es. el.offsetHeight + 'px' dentro un for con el.style.width). Ogni lettura forzata di proprietà come offsetHeight dopo una scrittura causa un reflow sincrono. Fai prima tutte le letture, poi tutte le scritture.
JavaScript Lazy Loading e Code Splitting per l’INP
Molto spesso il main thread è bloccato non da codice che usi subito, ma da codice che non serve all’interazione iniziale. Caricare tutto in bundle unico è il modo più sicuro per alzare l’INP.
Code Splitting (con Webpack, Vite, o import() dinamico) carica solo il codice necessario per la pagina corrente. Per componenti interattivi (modali, slider, form dinamici), caricali tramite import() al momento dell’interazione.
// Carica il modulo solo quando l'utente clicca
button.addEventListener('click', async () => {
const { showModal } = await import('./modal.js');
showModal();
});
Lazy loading degli script (con attributo async o defer): gli script che non bloccano il rendering vengono eseguiti in coda, senza fermare il main thread. Anche qui, schema:
Sponsored Protocol
Perché funziona: async carica e poi esegue appena pronto, ma non blocca il parsing HTML. defer esegue dopo che il documento è stato completamente analizzato. In entrambi i casi, il main thread non si ferma per scaricare lo script.
Ottimizzazione Specifica per l’INP su Mobile
Su mobile il problema è amplificato: CPU meno potente, spesso senza fan, e connessioni lente. Il main thread è più fragile. Ecco le regole extra che applichiamo ai progetti mobile-first:
- Limitare i listener sugli eventi touch: usa
passive: trueper non bloccare lo scrolling. - Evitare
setIntervalpesanti: megliorequestAnimationFrameper animazioni, esetTimeoutcon controllo di visibilità (Intersection Observer) per update periodici. - Comprimere e posticipare le font: le font caricate via
@font-facepossono bloccare il rendering. Usafont-display: swape carica solo il set necessario per la parte interattiva. - Gestire il recupero dei dati in modo asincrono: le chiamate API non devono mai essere sincrone. Anche
fetchè asincrona, ma se poi fai un lungo ciclo di parsing del JSON mentre blocchi, il danno è lo stesso.
Strumenti di Misura e Debug Avanzati
Oltre ai tool già citati, per l’INP specifico usiamo:
- Lighthouse Report (quella di Chrome): seleziona “Performance” e guarda la sezione “Interaction to Next Paint”. A volte dà suggerimenti specifici.
- WebPageTest: ha una scheda “Filmstrip” che mostra esattamente quando avvengono le interazioni e quanto tempo passa prima della risposta visiva.
- Performance Observer API: per tracciare le metriche in tempo reale nel browser.
// Esempio di PerformanceObserver per INP
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('INP entry:', entry.duration, entry.name);
}
});
observer.observe({ type: 'first-input', buffered: true });
// Per INP completo (non solo first input) usa type: 'event' con durationThreshold >= 0
IN Sintesi — Cosa Fare Adesso
- Misura l’INP reale: usa Search Console e Web Vitals library per capire se il tuo sito ha problemi su pagine specifiche.
- Identifica i long task: apri DevTools, registra un profilo interattivo, cerca task >50 ms.
- Suddividi e delega: spezza cicli lunghi con
setTimeouto sposta i calcoli in Web Worker. - Ottimizza gli event listener: delega, evita reflow forzati, usa
passiveper touch. - Applica code splitting e lazy loading: carica solo il JavaScript necessario per la prima interazione.
- Testa su dispositivo reale: un emulatore non basta. Prova su un telefono di fascia media con connessione 3G.
L’INP non è un problema da “aggiustare una volta”. È un attrito continuo: ogni nuovo script, ogni nuova interazione può peggiorarlo. Noi di Meteora Web controlliamo questa metrica ogni settimana nei progetti che seguiamo, perché un millisecondo risparmiato sul main thread è un cliente che non abbandona il carrello.
Sponsored Protocol
Se vuoi approfondire il contesto completo delle performance web, leggi la nostra Guida Pillar ai Core Web Vitals e PageSpeed.