Découvrez les phases libuv, la vraie différence entre setImmediate et setTimeout, et comment détecter un event loop bloqué.
Ouvrir cette leçon dans KodokonNode.js délègue sa boucle d'événements à libuv, qui l'organise en phases distinctes : timers (setTimeout, setInterval), pending callbacks, idle/prepare (interne), poll (I/O : sockets, fichiers), check (setImmediate) et close callbacks. À chaque tour, la boucle traverse ces phases dans cet ordre. Détail que peu de développeurs connaissent : entre chaque callback, Node vide d'abord la file de process.nextTick, puis les microtâches des promesses - et ce depuis Node 11, après chaque callback et non plus seulement entre les phases.
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"));
});Au démarrage du script, l'ordre entre setTimeout(fn, 0) et setImmediate(fn) est non déterministe : setTimeout(fn, 0) est en réalité plafonné à 1 ms, et si la boucle atteint la phase timers en moins d'une milliseconde, le timer n'est pas encore éligible. Dans un callback d'I/O en revanche, vous êtes dans la phase poll : la phase check (setImmediate) arrive toujours avant le retour à la phase timers. io immediate s'affiche donc systématiquement avant 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, microtaskPiège subtil : au niveau supérieur d'un module ES, cet ordre s'inverse - les microtâches des promesses passent avant process.nextTick. La raison : l'évaluation d'un module ES est elle-même exécutée comme un job de promesse, la file des microtâches est donc déjà en cours de drainage quand votre code synchrone se termine. En CommonJS, nextTick garde la priorité même au niveau supérieur. Vérifiez toujours ce genre d'ordre dans le format de module que vous déployez réellement.
L'event loop tourne sur un seul thread : tout calcul synchrone long - JSON.parse d'un gros payload, expression régulière catastrophique, chiffrement synchrone - gèle toutes les requêtes en cours. En production, mesurez la latence de la boucle avec monitorEventLoopDelay : un p99 au-dessus de quelques dizaines de millisecondes signale un blocage. Le code suivant provoque un blocage volontaire puis le mesure.
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);