Il cuore di Node.js: L'Event Loop
Scopri come l'Event Loop permette a Node.js di gestire migliaia di operazioni contemporaneamente pur essendo single-threaded.
Node.js non usa un thread per ogni utente. Usa un unico thread e un ciclo infinito chiamato Event Loop per orchestrare tutto.
Abbiamo visto che Node.js è veloce grazie al suo modello I/O non bloccante. Ma come fa un unico filo di esecuzione (single-thread) a gestire migliaia di connessioni senza andare in tilt? La risposta risiede in un meccanismo chiamato Event Loop.
Il Cameriere Perfetto: Un’analogia reale
Immagina un ristorante con un solo cameriere (l’Event Loop) e un cuoco (il Sistema Operativo/Libuv).
- L’Ordine: Il cameriere prende l’ordine al Tavolo A.
- Delega: Invece di aspettare in cucina che il piatto sia pronto, consegna la comanda e corre subito al Tavolo B.
- L’Evento: Quando il piatto del Tavolo A è pronto, il cuoco suona un campanello (genera un evento).
- Callback: Appena il cameriere finisce di servire chi ha davanti, sente il campanello e porta il piatto al Tavolo A.
Questo cameriere è instancabile perché non rimane mai con le mani in mano ad aspettare.
Come funziona tecnicamente
L’Event Loop monitora costantemente lo stato del programma muovendosi tra diverse “code” di messaggi. Ogni giro completo del ciclo è chiamato Tick.
È dove vengono eseguite le funzioni “normali” (sincrone). Se una funzione è qui, l’Event Loop è occupato.
Una coda speciale ad altissima priorità. Qui vivono le Promise. Node.js svuota sempre questa coda prima di passare alla successiva.
Qui finiscono le funzioni asincrone pronte per essere eseguite, come un
setTimeout scaduto o un’operazione su file conclusa.
Un esempio pratico: Chi arriva primo?
Anche se JavaScript esegue il codice riga per riga, l’Event Loop può cambiare l’ordine delle “apparenze”. Guarda questo codice:
console.log("1. Inizio")
setTimeout(() => {
console.log("2. Timeout (5 secondi)")
}, 5000)
fetch("https://api.esempio.it/").then(() => {
console.log("3. Risposta dalla rete")
})
console.log("4. Fine")
Cosa succede “sotto il cofano”?
- Sincrono: Stampa “1. Inizio”.
- Asincrono (Timer): Trova
setTimeout. Delega il timer a Node.js e passa oltre. - Asincrono (Microtask): Trova la
fetch. Delega la richiesta e continua. - Sincrono: Stampa “4. Fine”.
Appena il Call Stack è vuoto:
- La callback della fetch (Microtask) ha la priorità e viene eseguita non appena i dati tornano.
- Il
setTimeout(Task) viene eseguito solo allo scadere del tempo.
Output finale:
1. Inizio
4. Fine
3. Risposta dalla rete
2. Timeout (5 secondi)
“Don’t Block the Event Loop”. Poiché c’è un solo thread, se scrivi un calcolo che dura 10 secondi, il cameriere rimarrà “congelato” e il server non risponderà a nessun altro utente.
Le priorità nascoste: nextTick e setImmediate
Finora abbiamo detto che l’Event Loop segue l’ordine: Call Stack → Microtask Queue → Task Queue. Questa regola vale, ma Node.js aggiunge due strumenti che modificano le priorità e che troverai spesso nel codice reale.
- process.nextTick: esegue la callback prima ancora che la Microtask Queue venga processata. È il punto più prioritario dell’intero sistema, specifico di Node.js.
- setImmediate: esegue la callback nella prossima iterazione dell’Event Loop, dopo che tutte le callback I/O di questo giro sono state processate.
console.log("1. Sincrono")
setImmediate(() => {
console.log("4. setImmediate")
})
Promise.resolve().then(() => {
console.log("3. Promise (Microtask)")
})
process.nextTick(() => {
console.log("2. nextTick")
})
console.log("...ancora sincrono")
Output:
1. Sincrono
...ancora sincrono
2. nextTick
3. Promise (Microtask)
4. setImmediate
process.nextTick garantisce che una callback venga eseguita
subito dopo la funzione corrente ma prima di qualsiasi I/O — utile per
propagare errori o emettere eventi in modo prevedibile.
setImmediate è preferibile quando vuoi cedere il controllo
all’Event Loop e permettergli di gestire le operazioni pendenti prima di
continuare.
Cosa succede se blocchi davvero il loop?
La regola d’oro è chiara, ma vediamo le conseguenze concrete. Se esegui un calcolo pesante nel thread principale, ogni altra connessione deve aspettare — non importa quante siano.
import { createServer } from "node:http"
const server = createServer((req, res) => {
if (req.url === "/blocca") {
// Calcolo CPU che congela il loop per 3 secondi
const inizio = Date.now()
while (Date.now() - inizio < 3000) {}
res.end("Finito (ma hai bloccato tutti gli altri!)")
} else {
res.end("Risposta rapida")
}
})
server.listen(3000)
Apri due tab nel browser simultaneamente — una su /blocca, una su /. La seconda aspetterà i 3 secondi interi prima di ricevere risposta, anche se non ha nulla a che fare con il calcolo.
Per operazioni CPU intensive (parsing di file enormi, calcoli matematici, elaborazione di immagini) Node.js offre i Worker Threads — thread separati che non intaccano l’Event Loop principale. Sono uno strumento avanzato, ma ora sai esattamente perché esistono.
Hai capito come funziona l’Event Loop, ma come si scrive codice che lo sfrutta al meglio? Nel prossimo articolo ripercorreremo l’evoluzione dell’asincronia: dalle Callback fino al moderno async/await.