Pourquoi l’event loop est essentielle
Node.js exécute votre JavaScript sur un seul thread principal, et pourtant, un seul serveur peut gérer des milliers de connexions simultanées. C’est possible parce que l’event loop n’attend jamais. Lorsqu’une opération risquerait de bloquer, Node la délègue au système d’exploitation ou à un pool de threads et continue d’effectuer d’autres tâches. Une fois le résultat prêt, un callback est mis en file d’attente et la boucle l’exécute.
Comprendre l’event loop permet d’expliquer nombre de comportements de Node : pourquoi une lecture de fichier synchrone fige l’intégralité du serveur, pourquoi une promesse est logguée avant un timer avec un délai nul, et pourquoi les tâches lourdes pour le CPU nécessitent un worker. C’est le modèle mental le plus important pour écrire du code Node performant.
La boucle et ses phases
L’event loop est un cycle de phases. Chaque phase possède une file d’attente de callbacks, et la boucle les traite avant de passer à la suivante.
- Timers — callbacks planifiés par
setTimeoutetsetIntervalarrivés à échéance. - Pending callbacks — callbacks système différés, comme certaines erreurs TCP.
- Idle, prepare — maintenance interne.
- Poll — récupère les nouveaux événements I/O et exécute leurs callbacks. Si rien n’est prêt, elle peut bloquer brièvement à cette étape.
- Check — exécute les callbacks
setImmediate. - Close callbacks — gère les événements tels que la fermeture d’un socket.
La boucle continue de tourner tant qu’il y a du travail à effectuer. Lorsqu’il n’y a plus de timers, plus d’I/O en attente et plus de handles, le processus s’arrête.
Microtasks : nextTick et promises
Entre les phases, et après chaque callback, Node vide deux files d’attente prioritaires :
- Les callbacks
process.nextTick, qui s’exécutent en premier. - Les callbacks de Promise, qui s’exécutent après nextTick.
Toutes deux sont entièrement vidées avant que la boucle n’avance, ce qui a deux conséquences. Premièrement, elles s’exécutent avant les timers et les callbacks d’I/O planifiés précédemment. Deuxièmement, un nextTick récursif ou une chaîne de promises infinie peut affamer la boucle (starve the loop), empêchant ainsi l’exécution de l’I/O.
// order.js
console.log("1: synchronous");
setTimeout(() => console.log("5: timers"), 0);
setImmediate(() => console.log("6: check"));
Promise.resolve().then(() => console.log("3: promise"));
process.nextTick(() => console.log("2: nextTick"));
Le résultat est 1, 2, 3, puis 5 et 6. L’ordre de 5 et 6 peut varier au niveau racine (top level) car il dépend du temps nécessaire pour atteindre la phase des timers ; à l’intérieur d’un callback d’I/O, setImmediate s’exécute toujours avant setTimeout.
Le pool de threads de libuv
Certaines opérations ne peuvent pas être effectuées de manière asynchrone par l’OS de façon portable : l’accès au système de fichiers, les résolutions DNS et certaines fonctions cryptographiques. libuv exécute ces tâches sur un pool de threads, qui compte quatre threads par défaut, configurable via UV_THREADPOOL_SIZE.
C’est pourquoi fs.readFile est non-bloquant pour votre code, même s’il n’est pas piloté par des événements en interne. Cela signifie également qu’une rafale d’opérations sur les fichiers peut s’accumuler dans une file d’attente derrière un pool de quatre threads. Les API async libèrent le thread principal ; le pool, lui, gère l’attente.
Bloquer l’event loop
Tout ce qui est synchrone et lent sur le thread principal bloque absolument tout : les autres requêtes, les timers et les E/S. Les causes les plus fréquentes sont :
readFileSyncet autres appels de fichiers synchrones dans les gestionnaires de requêtes.- De gros
JSON.parseouJSON.stringifysur des payloads volumineux. - La cryptographie synchrone, comme
pbkdf2Syncavec un nombre élevé d’itérations. - Les boucles serrées, le backtracking de regex et les transformations de données lourdes.
- Les bibliothèques bloquantes qui effectuent un travail CPU sur le thread principal.
Mesurez plutôt que de deviner : le délai de l’event loop (event loop delay) et l’utilisation de l’event loop (event loop utilization) vous indiquent combien de temps la boucle reste bloquée. Une augmentation du délai sous charge est le signal d’alerte.
Décharger l’event loop
Lorsque le travail est réellement limité par le CPU (CPU-bound), déplacez-le :
- worker_threads pour les calculs au sein du même processus, via le passage de messages.
- child processes pour les tâches plus lourdes ou isolées.
- Un service séparé pour les charges de travail les plus massives, avec un scaling indépendant.
- Le chunking : divisez les tâches volumineuses en petits morceaux et laissez le contrôle entre chaque segment.
Pour les entrées/sorties (I/O), utilisez les API async et les streams afin que la loop reste libre.
Bonnes pratiques
- Utilisez des API async dans les gestionnaires de requêtes ; ne bloquez jamais la boucle.
- Laissez la priorité aux E/S avec
setImmediateplutôt qu’avec desnextTickrécursifs. - Déplacez les tâches gourmandes en CPU vers des worker threads ou des processus enfants.
- Utilisez des flux (streams) pour les données volumineuses au lieu de tout mettre en mémoire tampon.
- Ajustez
UV_THREADPOOL_SIZEuniquement après avoir effectué des mesures. - Surveillez le délai et l’utilisation de la boucle d’événements en production.
- Gardez des chaînes de callbacks courtes pour éviter que les microtâches ne privent les E/S de ressources.
Erreurs courantes
- Utiliser
readFileSyncdans un “hot path” (chemin critique). - Des
process.nextTickrécursifs qui saturent la boucle. - Supposer que Node.js est entièrement mono-thread et ignorer le pool de threads.
- Effectuer des calculs lourds en ligne au lieu de les déléguer.
- S’attendre à ce que
setTimeout(fn, 0)s’exécute avant les callbacks d’I/O. - Ignorer le délai de l’event loop jusqu’à ce que des problèmes de latence apparaissent.
Et après ?
L’event loop est le cœur de Node.js. Poursuivez avec les Streams pour les E/S par morceaux (chunked I/O) et le File System pour l’accès asynchrone aux fichiers. Revenez aux Bases de Node.js si le runtime est encore nouveau pour vous, puis mesurez le délai de l’event loop sur l’un de vos services.