La boucle d'événements JavaScript qui pilote votre page web se comporte de manière très différente de celle qui alimente un serveur Node.js, et ce décalage peut figer une interface utilisateur ou saturer les E/S si vous n'y prenez pas garde. Savoir où les deux divergent est essentiel pour quiconque écrit du code asynchrone destiné à s'exécuter dans les deux environnements.
Pourquoi cette distinction est importante
La boucle d'événements n'est pas définie par la spécification ECMAScript ; elle réside dans l'hôte. Les navigateurs doivent maintenir la réactivité d'une page tout en effectuant le rendu des images (frames), tandis que Node.js est conçu autour des E/S non bloquantes. Mélanger des modèles qui fonctionnent dans un hôte avec l'autre peut produire des bugs difficiles à reproduire : une longue chaîne de promesses peut bloquer le rendu (repaint) d'un navigateur, tandis qu'une boucle process.nextTick non contrôlée peut empêcher Node d'atteindre ses phases d'E/S.
La boucle par tours du navigateur
Dans un navigateur, la boucle exécute un cycle unique qui entrelace l'exécution des tâches, le vidage des microtâches et le rendu :
- Exécuter une macrotâche (un gestionnaire de clic, un
setTimeout, etc.). - Vider toutes les microtâches (promesses,
queueMicrotask). - Si une image est due, peindre et composer pour atteindre la cible de 60 fps.
- Répéter.
Deux API offrent aux développeurs des points d'ancrage (hooks) explicites dans ce cycle :
requestAnimationFrame– appelé juste avant que le navigateur ne peigne. C'est l'endroit idéal pour le travail d'animation car le rappel (callback) s'exécute après les microtâches actuelles mais avant l'image suivante.requestIdleCallback– invoqué lorsque le navigateur n'a pas de travail de haute priorité. Il est utile pour les tâches à faible impact telles que l'analyse (analytics) ou le préchargement de données.
Piège : l'asphyxie des microtâches
Comme le navigateur vide la file d'attente des microtâches avant de procéder au rendu, une longue chaîne de promesses peut empêcher l'interface utilisateur de s'afficher. La pile d'appels (call stack) n'est pas bloquée ; la page n'atteint simplement jamais l'étape de rendu, ce qui donne à l'utilisateur l'impression d'un gel.
La boucle de Node pilotée par libuv
Node.js délègue sa boucle à libuv, une bibliothèque C qui divise le travail en phases distinctes, chacune ayant sa propre file d'attente :
- Timers – rappels (callbacks) de
setTimeoutetsetInterval. - Pending callbacks – rappels d'E/S différés qui ont déjà été complétés au niveau du système d'exploitation.
- Poll – récupère les nouveaux événements d'E/S (lectures de fichiers, données réseau).
- Check – exécute les rappels de
setImmediate. - Close callbacks – se déclenche lorsqu'un socket ou un handle se ferme.
Deux constructions se situent en dehors de cet ordre de phases :
process.nextTick– s'exécute avant la file d'attente des microtâches, immédiatement après la fin de l'opération en cours.
Piège : l'asphyxie des E/S
Si une fonction planifie de manière répétée process.nextTick sans céder la main, Node ne progresse jamais au-delà de l'étape « next-tick ». Les requêtes réseau, les lectures de fichiers et les minuteurs restent inactifs, provoquant des pics de latence côté serveur ou des blocages complets.
setImmediate vs. setTimeout en pratique
Les deux planifient des rappels pour l'itération suivante, mais leur ordre relatif dépend de l'endroit où ils sont appelés :
- Code de haut niveau – l'ordre n'est pas garanti ; il dépend de la rapidité avec laquelle le processus démarre.
- À l'intérieur d'un rappel d'E/S – l'ordre est déterministe :
setImmediates'exécute avantsetTimeout(fn, 0). Une fois la phase Poll terminée, libuv passe à la phase Check (où se trouvesetImmediate) avant de réentrer dans la phase Timers pour un timeout à délai nul.
Cette subtilité est importante lorsque vous dépendez d'un séquençage précis, comme le nettoyage d'une ressource juste après la fin d'une lecture.
Différences clés en un coup d'œil
- Objectif : Les navigateurs privilégient les mises à jour visuelles ; Node privilégie la disponibilité des E/S.
- Point d'ancrage de rendu :
requestAnimationFrame(navigateur uniquement). - Point d'ancrage spécifique à une phase :
setImmediate(Node uniquement, s'exécute dans la phase Check). - File d'attente haute priorité :
process.nextTick(Node uniquement, s'exécute avant les microtâches). - Risque d'asphyxie : Longues chaînes de promesses dans les navigateurs ;
process.nextTicksans limite dans Node.
À surveiller ensuite
Si vous maintenez une base de code qui s'exécute dans les deux environnements (par exemple, des bibliothèques isomorphes), auditez tout endroit où vous :
- Enchaînez de nombreuses promesses sans céder la main à la boucle d'événements. Insérez
await new Promise(r => setTimeout(r, 0))ou utilisezrequestIdleCallbackdans le navigateur pour donner une chance au moteur de rendu. - Utilisez
process.nextTickpour des tâches qui pourraient être différées. PréférezsetImmediateou une promesse classique lorsque vous n'avez pas besoin de l'urgence du « next-tick ». - Supposez que
setTimeout(fn, 0)etsetImmediatesont interchangeables. Testez l'ordre à l'intérieur des rappels d'E/S si la séquence est importante.
À retenir
La boucle d'événements (event loop) est un ordonnanceur spécifique à l'hôte, et non une fonctionnalité universelle de JavaScript. Les navigateurs intègrent le rendu dans la boucle ; Node isole les E/S dans les phases de libuv. Une mauvaise utilisation des mécanismes de priorité — les microtâches dans le navigateur, process.nextTick dans Node — peut priver de ressources la partie du système que chaque environnement est conçu pour servir. Alignez vos modèles asynchrones sur le modèle de boucle de l'hôte, et vous éviterez aussi bien les pages figées que les serveurs bloqués.
