Event Loop di JavaScript, Microtask e Macrotask
Scopri come funziona l'event loop di JavaScript e come vengono pianificati microtask (Promise) e macrotask (timer, eventi), con esempi eseguibili sull'ordine del codice asincrono.
JavaScript esegue il codice su un singolo thread: una cosa per volta, dall'alto verso il basso. Eppure riesce comunque a recuperare dati, avviare timer e rispondere ai click senza bloccarsi. Il meccanismo che rende tutto ciò possibile è l'event loop, e il lavoro che pianifica è suddiviso in due tipi di operazioni: microtask e macrotask. Questa pagina spiega cosa sono, in quale ordine vengono eseguiti e le insidie più comuni — ogni esempio è eseguibile così puoi verificare l'output da solo.
Come Funziona l'Event Loop
L'event loop è il pianificatore che decide quale pezzo di codice eseguire successivamente. Per comprenderlo bastano tre componenti:
- Call stack — dove viene effettivamente eseguito il codice. Le funzioni vengono aggiunte in cima quando vengono chiamate e rimosse quando ritornano. JavaScript esegue tutto ciò che si trova nello stack fino al completamento prima di fare qualsiasi altra cosa; questa è la regola del run-to-completion.
- Heap — la memoria in cui risiedono gli oggetti. Non è direttamente coinvolto nella pianificazione, ma è il terzo componente che ci si aspetta di vedere nominato.
- Code delle task — lavoro in attesa che lo stack si svuoti. Ce ne sono due: la coda dei macrotask (timer, eventi UI, I/O) e la coda dei microtask (callback delle Promise e
queueMicrotask).
Un ciclo dell'event loop funziona così:
- Esegui il task corrente nello stack fino a quando lo stack è completamente vuoto.
- Svuota l'intera coda dei microtask — inclusi eventuali microtask aggiunti durante lo svuotamento.
- (Nel browser) renderizza eventuali aggiornamenti visivi in attesa.
- Preleva un macrotask dalla coda dei macrotask ed eseguilo, poi torna al punto 2.
L'asimmetria fondamentale: dopo ogni macrotask il motore svuota tutti i microtask, ma preleva solo un macrotask per turno di loop. Questa singola regola spiega quasi ogni sorpresa nell'ordinamento che incontrerai.
Ecco la dimostrazione più semplice possibile, che usa setTimeout per pianificare un macrotask:
In questo esempio:
console.log('Start');viene eseguito per primo, stampando "Start" nella console.setTimeoutpianifica un callback da eseguire dopo almeno 1000 millisecondi. Ritorna immediatamente e non blocca le righe successive.console.log('End');viene eseguito immediatamente, stampando "End".- Solo dopo che lo script sincrono termina (e il ritardo è trascorso) l'event loop preleva il callback di
setTimeoutdalla coda dei macrotask e lo esegue, stampando "Timeout Callback".
L'output è Start, End, poi Timeout Callback — il callback del timer attende anche se è scritto nel mezzo. Il callback di setTimeout è un macrotask: viene eseguito solo dopo che lo script in esecuzione e tutti i microtask in attesa sono terminati. Questo è ciò che mantiene la pagina reattiva — il codice sincrono non deve mai aspettare un timer o una richiesta di rete.
Microtask vs. Macrotask
Cosa sono i Macrotask?
Un macrotask (chiamato anche semplicemente "task") è un'unità di lavoro singola e autonoma che il motore preleva una volta per turno di loop. Le sorgenti più comuni sono:
setTimeout/setInterval: timer che eseguono un callback dopo un ritardo o ripetutamente.- Eventi DOM: un handler di
click,scrolloinput. - I/O: risposte di rete, letture di file e simili.
Il motore esegue esattamente un macrotask, poi svuota tutti i microtask, poi (nel browser) può eseguire il rendering, prima di prelevare il macrotask successivo. Quindi i macrotask non vengono mai eseguiti consecutivamente senza che la coda dei microtask venga svuotata nel mezzo.
Cosa sono i Microtask?
Un microtask è un'operazione breve che il motore vuole completare non appena l'unità di codice corrente termina — prima di cedere il controllo al prossimo macrotask o al rendering. Provengono da:
- Callback delle Promise: le funzioni passate a
.then(),.catch()e.finally(), oltre al corpo di una funzioneasyncdopo unawait. queueMicrotask(fn): una funzione integrata che pianifica una funzione direttamente nella coda dei microtask.
La differenza cruciale: dopo il task corrente, il motore svuota l'intera coda dei microtask prima di fare qualsiasi altra cosa. Se un microtask pianifica un altro microtask, anche quello nuovo viene eseguito nello stesso svuotamento — prima che il prossimo macrotask abbia un turno.
Esempi di Codice Concreti
Esempio 1: Un timer è un macrotask
Immagina di voler mostrare un messaggio dopo 2 secondi. La riga di pianificazione viene eseguita adesso; il callback viene parcheggiato nella coda dei macrotask fino a quando il ritardo non è trascorso e lo stack è libero.
Spiegazione: setTimeout ritorna immediatamente, quindi entrambe le righe console.log esterne vengono eseguite per prime. Il callback è un macrotask che viene eseguito solo dopo che lo script sincrono termina e il timer scatta. In un browser tipicamente si aggiornerebbe il DOM all'interno del callback, ad esempio document.getElementById('message').textContent = 'Hello there!';.
Esempio 2: Il callback di una Promise è un microtask
Il callback .then() di una Promise già risolta non viene eseguito inline — viene accodato come microtask e viene eseguito una volta che il codice sincrono corrente termina.
Spiegazione: L'output è Before the promise, After the promise, poi Promise resolved (microtask). Anche se la Promise è già risolta, il suo callback .then() attende nella coda dei microtask fino a quando il codice sincrono è terminato — poi viene eseguito prima di qualsiasi timer.
Approfondimento sulla Priorità di Micro e Macro Task
I microtask hanno sempre priorità maggiore dei macrotask. Dopo che lo script corrente termina, il motore svuota ogni microtask in attesa prima di toccare un singolo macrotask — anche un setTimeout(..., 0) pianificato per primo. Nota nell'esempio seguente che Promise 2 concatenata, creata all'interno di un microtask, viene comunque eseguita prima di entrambi i timer, perché la coda dei microtask viene svuotata completamente prima che il loop vada avanti.
Output atteso:
Start
End
Promise 1
Promise 2
Timeout 1
Timeout 2Questo mostra che i microtask vengono eseguiti immediatamente dopo il codice sincrono, anche prima dei timer pianificati per lo stesso momento. La prioritizzazione significa che gli aggiornamenti basati su Promise si completano il prima possibile.
Un'Insidia: La Starvation dei Microtask
Poiché il motore svuota l'intera coda dei microtask prima del prossimo macrotask o di un rendering, un microtask che continua a pianificare altri microtask può bloccare tutto il resto — i timer non scattano mai e la pagina non può ridisegnarsi. Questo si chiama starvation dei microtask:
I cinque microtask vengono eseguiti tutti prima del callback di setTimeout, anche se il timer è stato pianificato per primo. In un'applicazione reale, una versione illimitata di questo loop congelerebbe l'interfaccia utente. La soluzione è suddividere il lavoro di lunga durata in macrotask (ad esempio setTimeout(..., 0)), che consente all'event loop di eseguire il rendering e gestire gli eventi tra un blocco e l'altro.
Quando Usare Quale
- Usa i microtask (Promise,
queueMicrotask) quando vuoi che il codice venga eseguito non appena l'operazione corrente termina ma comunque in modo asincrono — come reagire ai dati subito dopo che unafetchsi risolve. - Usa i macrotask (
setTimeout, suddivisione del lavoro tra timer) quando vuoi deliberatamente cedere il controllo al browser in modo che possa eseguire il rendering o gestire l'input prima di continuare — ad esempio, suddividere un'elaborazione pesante in modo che la pagina rimanga reattiva.
Conclusione
L'event loop esegue il codice sincrono fino al completamento, poi svuota tutti i microtask, poi preleva un macrotask, e ripete. I microtask (callback delle Promise, queueMicrotask) vengono sempre eseguiti prima del prossimo macrotask (timer, eventi, I/O). Interiorizzare questa singola regola permette di prevedere l'ordine esatto di qualsiasi codice asincrono.
Per approfondire, continua con Promise, concatenazione di Promise, async/await e il capitolo dedicato ai microtask. Per le API dei timer utilizzate qui, consulta la pianificazione con setTimeout e setInterval.