Menu

Event loop Node.js : phases, nextTick et setImmediate

L'event loop de Node.js exécute vos callbacks par phases (timers, poll, check, close), et entre deux callbacks il vide la file process.nextTick puis la file des promesses. Lancez les exemples pour voir l'ordre exact.

Cette page contient des éditeurs exécutables. Modifiez, exécutez et voyez la sortie instantanément.

L'event loop de Node.js est la boucle qui décide quel callback s'exécute ensuite une fois votre code synchrone terminé. Il parcourt des phases dans un cycle fixe : timers (setTimeout, setInterval), pending callbacks, poll (les I/O comme la lecture de fichiers et les données réseau), check (setImmediate) et close callbacks. Après chaque callback, Node vide d'abord la file process.nextTick, puis la file des promesses, donc elles passent toujours avant que la boucle continue.

Le programme affiche sync, nextTick, promise, setTimeout, soit l'inverse de l'ordre d'écriture. Le code synchrone se termine toujours en premier, puis Node vide la file nextTick, puis la file des promesses, et seulement ensuite la boucle démarre et atteint la phase timers.

Les phases de l'event loop

La boucle de Node vient de libuv, la bibliothèque C que Node utilise pour les I/O. Avant le premier tour, elle exécute les timers déjà échus, puis chaque tour exécute ces phases dans l'ordre :

  1. Pending callbacks : quelques callbacks système reportés du tour précédent, comme certaines erreurs réseau.
  2. Poll : les nouveaux événements d'I/O et leurs callbacks (une lecture de fichier terminée, des données arrivées sur un socket). Quand rien d'autre n'est prévu, la boucle attend ici.
  3. Check : les callbacks de setImmediate.
  4. Close callbacks : les événements 'close', par exemple un socket détruit.
  5. Timers : les callbacks de setTimeout et setInterval dont l'heure est venue.

Beaucoup de guides placent les timers en premier à chaque tour, ce qui était le fonctionnement de Node avant la version 20 (libuv 1.45). Les callbacks s'exécutent dans le même ordre dans les deux cas : les timers d'un tour sont suivis du poll du tour suivant.

Deux files se trouvent en dehors des phases : la file nextTick et la file des microtâches (les callbacks de promesses et queueMicrotask). Node vide les deux après chaque callback, quelle que soit la phase, nextTick en premier. La boucle continue de tourner tant qu'il reste quelque chose à attendre (un timer, un fichier ou un socket ouvert, un serveur), et le processus se termine quand il ne reste plus rien.

C'est la boucle propre à Node. Le navigateur partage l'idée de tâches et de microtâches, mais n'a pas de phases, pas de setImmediate et pas de process.nextTick ; la page sur l'event loop JavaScript présente la vue côté langage.

process.nextTick ou promesses

Les deux s'exécutent avant que la boucle continue, mais ils ne se mélangent pas. Node exécute toute la file nextTick, y compris les ticks ajoutés pendant son exécution, et seulement ensuite la file des promesses. Devinez le résultat :

L'ordre est tick 1, tick 2, tick from tick, promise 1, promise 2. Le tick programmé à l'intérieur d'un tick rejoint la même file et passe quand même avant toute promesse, alors que la promesse a été créée en premier.

Au niveau supérieur d'un module ES (un fichier qui utilise import), les deux s'inversent : le code du module tourne lui-même dans un job de promesse, donc ses callbacks de promesses se terminent avant que Node n'arrive à la file nextTick. À l'intérieur des callbacks, la règle « nextTick d'abord » vaut dans les deux systèmes de modules.

setImmediate ou setTimeout(fn, 0)

setTimeout(fn, 0) veut en réalité dire « après au moins 1 ms, dans la phase timers ». setImmediate(fn) veut dire « dans la prochaine phase check ». Lequel passe en premier dépend de l'endroit où vous les appelez.

Dans un callback d'I/O, l'ordre est fixe. Le callback s'exécute dans la phase poll, la phase check vient juste après, et la phase timers n'arrive qu'ensuite :

Il affiche nextTick, setImmediate, setTimeout, à chaque fois. C'est pour cela que setImmediate est le bon choix pour « exécuter ceci après le travail d'I/O en cours ».

Dans le module principal, en dehors de tout callback, l'ordre des deux n'est pas fixe :

// Order depends on how fast the process started
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

Si au moins 1 ms s'est écoulée quand la boucle démarre, le timer est échu et s'affiche en premier ; sinon setImmediate gagne. N'écrivez pas de code qui dépend de l'un ou l'autre résultat.

S'exécuteÀ utiliser pour
process.nextTick(fn)Juste après l'opération en cours, avant les promessesÉmettre un événement ou une erreur après que l'appelant a attaché ses listeners
Promise.then, queueMicrotaskUne fois la file nextTick videLe code asynchrone ordinaire
setImmediate(fn)Phase check, après les callbacks d'I/ORendre la main à la boucle entre deux morceaux de travail
setTimeout(fn, 0)Phase timers, au moins 1 ms plus tardLes délais, quand le moment exact n'a pas d'importance

Bloquer l'event loop

Comme il n'y a qu'un thread JavaScript, un callback qui dure longtemps arrête tout le reste : les timers, les callbacks d'I/O et, dans un serveur, toutes les autres requêtes. Le délai d'un timer est un minimum, pas une garantie.

loop finished s'affiche d'abord, puis true : le timer était échu au bout de 10 ms mais n'a pas pu s'exécuter avant que la boucle active rende le thread. Dans un serveur, la même boucle dans un handler de route gèle toutes les autres requêtes pendant une demi-seconde.

Pour un long travail, la solution est de le découper et de laisser un tour à la boucle entre les morceaux avec setImmediate :

Le timer passe après la partie 1. Remplacez setImmediate(() => work(part + 1)) par work(part + 1) et relancez : les trois parties s'exécutent d'un coup et timer s'affiche en dernier.

Pour un travail lourd de bout en bout, comme le traitement d'images, le découpage ne suffit pas : sortez-le du thread principal avec worker_threads.

Erreurs fréquentes

Appeler process.nextTick de façon récursive. Un callback nextTick qui programme sans cesse un autre nextTick ne laisse jamais la boucle continuer, et les timers et les I/O sont affamés. Utilisez setImmediate pour un travail répété.

Utiliser des API synchrones dans un serveur. fs.readFileSync, crypto.pbkdf2Sync et les gros appels à JSON.parse bloquent la boucle pour tous les utilisateurs. Utilisez les versions asynchrones dans les handlers de requêtes.

S'attendre à ce que setTimeout(fn, 0) s'exécute tout de suite. Il s'exécute après tout le code synchrone, chaque nextTick et chaque callback de promesse, et seulement quand la boucle atteint la phase timers.

Questions fréquentes

Node.js est-il single-thread ?

Votre JavaScript tourne sur un seul thread, donc deux callbacks ne s'exécutent jamais en même temps. Node lui-même utilise d'autres threads : libuv garde un pool (quatre threads par défaut) pour le système de fichiers, les résolutions DNS et une partie de la crypto, puis renvoie les résultats à la boucle.

Quand un programme Node.js se termine-t-il ?

Quand l'event loop n'a plus rien à attendre : aucun timer, aucune I/O en cours, aucun serveur ni socket ouvert. Un setInterval actif ou un serveur à l'écoute garde le processus en vie jusqu'à ce que vous l'arrêtiez ou le fermiez.

L'event loop de Node.js est-il le même que celui du navigateur ?

L'idée est la même, un thread qui exécute des callbacks en file d'attente, mais les détails diffèrent. Le navigateur n'a ni setImmediate ni process.nextTick et place le rendu entre les tâches, alors que Node suit les phases de libuv. La page sur l'event loop JavaScript couvre la partie commune, celle du langage.

Comment savoir si l'event loop est bloqué ?

Mesurez son retard : monitorEventLoopDelay() de perf_hooks enregistre le délai, et une moyenne de quelques dizaines de millisecondes signifie qu'un callback garde le thread trop longtemps. Lancez Node avec --cpu-prof pour savoir quelle fonction est en cause.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

Commencer