Il tuo codice JavaScript viene eseguito in ordine, ma solo fino a quando non incontri un'operazione asincrona. Una chiamata API, un timer, una Promise e l'ordine che credevi solido si rompe. Se non capisci l'Event Loop, ogni bug asincrono diventa un mistero che ti fa perdere ore.
Noi, di Meteora Web, lavoriamo ogni giorno su applicazioni JavaScript reali – piattaforme proprietarie in Laravel/Vue, e-commerce con React, tool di automazione. E lo vediamo: sviluppatori bravi a scrivere codice sincrono crollano quando devono gestire l'asincrono. Non perché sia complicato, ma perché manca la mappa mentale. Questa guida te la dà.
Che cos'è l'Event Loop e perché determina l'ordine di esecuzione?
JavaScript è single-thread: un solo Call Stack, una sola cosa alla volta. Ma il browser e Node.js non sono single-thread: hanno thread per I/O, timer, rete. L'Event Loop è il ponte tra il tuo codice sincrono e quelle operazioni esterne. Quando il Call Stack è vuoto, l'Event Loop decide cosa mettere dentro – e in che ordine.
Non è magia. È un ciclo:
- Esegui tutto ciò che è nel Call Stack fino a svuotarlo.
- Guarda la Microtask Queue: svuotala completamente.
- Poi guarda la Macrotask Queue: prendi il primo task e lo esegui.
- Ripeti.
Se non tieni a mente questo ciclo, le tempistiche di setTimeout e Promise restano un indovinello.
Sponsored Protocol
Cosa puoi fare ora: Apri la console del browser o Node.js. Scrivi:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2Risultato: 1, 4, 3, 2. Perché? La Promise (microtask) viene prima del setTimeout (macrotask). Se non capisci il perché, leggi il prossimo paragrafo.
Come funzionano Call Stack, Microtask Queue e Macrotask Queue?
Call Stack – il punto di partenza
Il Call Stack è una pila LIFO. Ogni funzione chiamata crea un frame. Quando una funzione termina, il frame viene rimosso. Fino a che non è vuoto, l'Event Loop non interviene. Una funzione che impiega 5 secondi blocca tutto – scroll, click, animazioni. Noi lo vediamo nei progetti che ci arrivano: un loop pesante in una pagina e-commerce e l'utente pensa che il sito sia rotto.
Microtask Queue – priorità massima
I microtask includono:
- Promise.then / catch / finally
- queueMicrotask()
- MutationObserver (browser)
- process.nextTick (Node.js)
Vengono eseguiti subito dopo ogni operazione del Call Stack, prima che l'Event Loop guardi i macrotask. Un microtask può generare altri microtask, e si svuota tutto prima di passare al prossimo macrotask. Ecco perché un Promise.then() ha la precedenza su setTimeout(fn, 0).
Sponsored Protocol
Macrotask Queue – il turno seguente
I macrotask includono:
- setTimeout / setInterval
- setImmediate (Node.js)
- eventi DOM (click, load, scroll) – gestiti dal browser come macrotask
- fetch / XMLHttpRequest – la risposta arriva come macrotask
L'Event Loop prende un solo macrotask per ciclo. Dopo averlo eseguito, torna a controllare i microtask. Questo meccanismo garantisce che i microtask non facciano morire di fame i macrotask, ma al contrario li ritardano fino alla fine della coda micro.
Cosa puoi fare ora: Scrivi questo test:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => {
console.log('C');
queueMicrotask(() => console.log('D'));
});
console.log('E');
// Output: A, E, C, D, BD (microtask generato da altro microtask) esce prima di B (macrotask).
Qual è l'ordine reale di esecuzione tra microtask e macrotask con esempi concreti?
L'errore più comune: credere che setTimeout(fn, 0) esegua subito dopo la riga successiva. Falso. Vede l'immagine:
- Codice sincrono nel Call Stack
- Quando finisce, tutti i microtask in coda
- Poi un macrotask
- Di nuovo microtask
- E così via
Immagina uno scenario reale: un e-commerce deve caricare dati prodotto (API) e subito dopo mostrare un popup di sconto. Se usi:
Sponsored Protocol
fetch('/api/prodotti').then(res => res.json()).then(prodotti => aggiornaCarrello(prodotti));
setTimeout(() => mostraPopup(), 0);Il popup apparirà solo dopo che tutti i .then() della fetch sono stati eseguiti – anche se la risposta arriva in 2 ms. Perché? La fetch restituisce una Promise; il .then() è un microtask. Il setTimeout è macrotask. Quindi, anche se la Promise si risolve subito, il setTimeout aspetta che tutti i microtask (inclusi quelli interni alla catena) siano completati.
Come gestirlo nella pratica? Se vuoi che il popup appaia il prima possibile, sposta la logica del popup in un microtask o usa requestAnimationFrame per il rendering. Ma attento: non abusare dei microtask perché potresti ritardare il rendering (i microtask bloccano il paint del browser).
Cosa puoi fare ora: Analizza il codice asincrono della tua applicazione. Cerca pattern come:
setTimeout(() => {
fetch('/api/dati').then(...);
}, 0);Probabilmente volevi eseguire il fetch subito, ma il setTimeout lo ritarda inutilmente. Sostituisci con:
Sponsored Protocol
queueMicrotask(() => fetch('/api/dati').then(...));
// oppure semplicemente fetch fuori dal timeout
Come evitare i blocchi del Call Stack e i memory leak?
Un Call Stack troppo pieno non è solo lento: blocca l'UI e impedisce la gestione di eventi. Una funzione ricorsiva senza condizione di uscita? Stack overflow immediato. Ma il problema più insidioso sono i loop sincroni lunghi: un calcolo pesante che occupa il Call Stack per secondi. L'Event Loop non può processare né microtask né macrotask. L'utente clicca e niente succede.
Soluzione: spezzare il lavoro in chunk usando setTimeout o requestIdleCallback. Esempio:
function processaLotto(lotto, indice) {
const chunk = lotto.slice(indice, indice + 1000);
chunk.forEach(processaElemento);
if (indice + 1000 < lotto.length) {
setTimeout(() => processaLotto(lotto, indice + 1000), 0);
}
}
processaLotto(grandeArray, 0);In questo modo, dopo ogni chunk, il Call Stack si libera e l'Event Loop può gestire altri compiti (click, animazioni, Promise).
Memory leak nei microtask: Attenzione a ricreare microtask all'infinito. Se in un .then() generi un'altra Promise che si risolve immediatamente, si crea un loop di microtask che non lascia mai spazio ai macrotask. L'applicazione sembra viva ma non reagisce più a eventi esterni (timer, scroll). Ecco un errore che abbiamo visto in produzione.
Sponsored Protocol
Cosa puoi fare ora: Controlla se nella tua base di codice esistono Promises ricorsive o catene di .then() che si chiamano senza condizioni di break. Usa un limite di profondità o un timeout esterno per uscire.
Cosa fare adesso
- Metti alla prova le tue conoscenze: Apri la console e lancia il codice dell'esempio all'inizio. Spiega a voce alta l'ordine di output. Se esiti, hai ancora bisogno di ripassare.
- Audita il tuo codice asincrono: Cerca tutti i setTimeout senza delay e chiediti: “È davvero necessario? Non posso usare una Promise o queueMicrotask?”
- Evita blocchi sincroni lunghi: Se hai loop che processano array con migliaia di elementi, spezzali in chunk.
- Verifica che i microtask non blocchino i macrotask: Controlla che non ci siano Promise ricorsive infinite.
- Approfondisci la documentazione ufficiale: Leggi il MDN sull'Event Loop per la versione browser e Node.js Event Loop per lato server.
- Leggi altri approfondimenti sul nostro sito: Se vuoi completare lo studio, consulta la nostra guida principale su JavaScript ES2024+.