Descubre las fases de libuv, la diferencia real entre setImmediate y setTimeout, y cómo detectar un event loop bloqueado.
Abrir esta lección en KodokonNode.js delega su event loop a libuv, que lo organiza en fases distintas: timers (setTimeout, setInterval), pending callbacks, idle/prepare (interna), poll (E/S: sockets, archivos), check (setImmediate) y close callbacks. En cada tick, el loop recorre estas fases en ese orden. Un detalle que pocos desarrolladores conocen: entre cada callback, Node primero vacía la cola de process.nextTick, luego las microtareas de promesas - y esto es así desde Node 11, después de cada callback en lugar de solo entre fases.
import { readFile } from "node:fs";
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
readFile(new URL(import.meta.url), () => {
setTimeout(() => console.log("io timeout"), 0);
setImmediate(() => console.log("io immediate"));
});Cuando arranca el script, el orden entre setTimeout(fn, 0) y setImmediate(fn) es no determinista: setTimeout(fn, 0) en realidad se ajusta a un mínimo de 1 ms, y si el loop llega a la fase de timers en menos de un milisegundo, el timer aún no es elegible. Dentro de un callback de E/S, en cambio, estás en la fase poll: la fase check (setImmediate) siempre viene antes de volver a la fase de timers. Por eso io immediate se imprime de forma consistente antes que io timeout.
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("sync");
}, 0);
// sync, nextTick, promise, microtaskUna trampa sutil: en el nivel superior de un módulo ES, este orden se invierte - las microtareas de promesas se ejecutan antes que process.nextTick. La razón: evaluar un módulo ES se ejecuta a su vez como un trabajo de promesa, por lo que la cola de microtareas ya se está vaciando cuando tu código síncrono termina. En CommonJS, nextTick conserva su prioridad incluso en el nivel superior. Verifica siempre este tipo de orden en el formato de módulo que realmente despliegas.
El event loop se ejecuta en un solo hilo: cualquier cálculo síncrono largo - un JSON.parse de un payload grande, una expresión regular catastrófica, un cifrado síncrono - congela todas las peticiones en curso. En producción, mide la latencia del loop con monitorEventLoopDelay: un p99 por encima de unas pocas decenas de milisegundos indica un bloqueo. El siguiente código provoca deliberadamente un bloqueo y luego lo mide.
import { monitorEventLoopDelay }
from "node:perf_hooks";
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
const end = Date.now() + 200;
while (Date.now() < end) {}
setTimeout(() => {
h.disable();
const ms = h.max / 1e6;
console.log(`max delay: ${ms.toFixed(1)} ms`);
}, 300);