Retour au cours

backend / nodejs

Runtime et Event Loop

Leçon 11 exercice

Explication

Un peu d'histoire

Node.js est présenté par Ryan Dahl en 2009. Son idée : réutiliser le moteur JavaScript V8 de Google (rendu open source en 2008 pour le navigateur Chrome) pour exécuter du JavaScript côté serveur, avec un fonctionnement non-bloquant basé sur une boucle d'événements (event loop) plutôt que sur un thread dédié par requête. Ce choix technique permet à un serveur Node.js de gérer un très grand nombre de connexions simultanées avec une consommation de ressources modérée.

Pourquoi apprendre Node.js aujourd'hui

Node.js a permis à JavaScript de sortir du navigateur et de devenir un langage "full-stack" : le même langage peut servir à la fois pour le frontend et pour le backend. Il est utilisé en production par des entreprises comme Netflix, PayPal, LinkedIn ou Uber. Son écosystème de paquets, npm, est le plus grand registre de bibliothèques logicielles au monde, ce qui permet de démarrer un projet backend très rapidement sans tout réécrire depuis zéro.

Ce que vous allez apprendre

  • Comprendre pourquoi Node.js est single-threaded pour ton code, mais gère des milliers de connexions
  • Distinguer une macrotask (setTimeout) d'une microtask (Promise) et connaître leur ordre d'exécution
  • Situer process.nextTick dans cet ordre de priorité
  • Repérer un calcul synchrone qui bloque l'event loop et savoir comment le corriger
  • Comprendre le rôle de libuv dans la délégation des opérations lentes (fichiers, réseau)

Dans quel contexte ?

Un endpoint /api/rapport d'une API Express calcule un rapport statistique directement dans le handler de la route, avec une boucle synchrone lourde sur plusieurs milliers d'enregistrements. Dès qu'un utilisateur appelle cette route, TOUTES les autres requêtes en cours sur le même serveur — même des requêtes simples comme /health — se figent pendant plusieurs secondes. Comprendre l'event loop permet de diagnostiquer précisément pourquoi, et de savoir où déporter ce calcul.

D'abord, un malentendu très répandu à corriger

Beaucoup de débutants pensent que Node.js est "multi-thread" parce qu'il gère des milliers de connexions en même temps. C'est en réalité l'inverse : ton code JavaScript s'exécute sur UN SEUL thread, du début à la fin.

Alors comment gérer autant de connexions avec un seul thread ? Node délègue les opérations lentes (lire un fichier, interroger une base, faire une requête réseau) à un système en coulisses appelé libuv, qui utilise lui un pool de threads séparés.

Pendant qu'une de ces opérations attend, ton thread principal reste libre. Il peut donc traiter d'autres requêtes sans rester bloqué à attendre une réponse.

Une image aide à visualiser ce mécanisme : le chef d'orchestre. Imagine un serveur de restaurant qui prend une commande, la transmet en cuisine, puis va immédiatement prendre la commande de la table suivante au lieu de rester planté à attendre que le plat soit prêt.

L'event loop fonctionne exactement pareil. Il exécute le code synchrone, puis, dès qu'il n'y a plus rien à faire immédiatement, il va chercher la prochaine tâche prête (un timer arrivé à échéance, une lecture de fichier terminée, une Promise résolue).

PrioritéType de tâcheExemple
1 (la plus haute)process.nextTickprocess.nextTick(() => ...)
2MicrotaskPromise.resolve().then(...)
3MacrotasksetTimeout(...), setImmediate(...)

Prérequis

Aucune connaissance préalable de Node.js n'est nécessaire pour cette leçon : une base de JavaScript (fonctions, Promises) suffit pour suivre les exemples.

Une fois ce principe compris, il reste un détail contre-intuitif à connaître : l'ordre d'exécution. Les microtasks (les Promises) sont TOUJOURS entièrement vidées avant de passer à la macrotask suivante, comme un setTimeout.

Et il y a encore plus prioritaire que les Promises. process.nextTick s'exécute avant même les microtasks classiques — ne pas connaître cet ordre mène à des bugs où le code semble s'exécuter "dans le désordre".

Il reste un dernier piège, le plus dangereux de tous : bloquer l'event loop. Puisque tout le code JS tourne sur un seul thread, un calcul synchrone lourd (une suite de Fibonacci récursive mal optimisée, par exemple) bloque LITTÉRALEMENT toutes les autres requêtes en cours, même celles qui n'ont rien à voir avec ce calcul.

La solution à ce piège n'est jamais d'optimiser le calcul, mais de le déplacer ailleurs. On le déporte vers un worker thread ou un service externe, une technique détaillée dans une leçon dédiée à la performance plus loin dans ce cours.

Piège fréquent

Un calcul récursif lourd (comme une suite de Fibonacci mal optimisée) exécuté directement dans un handler de route Express bloque TOUT le serveur, pas seulement cette requête. Pendant ce calcul, même un endpoint /health totalement indépendant ne répond plus, car un seul thread traite tout le code JavaScript.

Et la suite ? Cette première leçon pose les fondations conceptuelles de tout le reste : modules, serveur HTTP, streams — tout repose sur ce modèle d'exécution non-bloquant que tu viens de découvrir.

Commandes & code

Runtime et Event Loop

Node.js exécute du JavaScript hors navigateur grâce au moteur V8, avec un modèle non-bloquant, single-threaded pour le code applicatif.

js
// Node.js est single-threaded pour VOTRE code, mais délègue l'I/O à libuv (thread pool)
console.log("1 - début du script");

setTimeout(() => console.log("4 - timeout (macrotask)"), 0);

Promise.resolve().then(() => console.log("3 - promise (microtask)"));

console.log("2 - fin du script synchrone");

// Ordre d'exécution réel : 1, 2, 3, 4
// Les microtasks (Promises) sont TOUJOURS vidées avant la prochaine macrotask
js
// Les phases de l'event loop, simplifiées
// timers -> pending callbacks -> poll (I/O) -> check (setImmediate) -> close callbacks

const fs = require("fs");

console.log("A - synchrone");

setTimeout(() => console.log("B - setTimeout 0ms"), 0);
setImmediate(() => console.log("C - setImmediate"));

fs.readFile(__filename, () => {
  console.log("D - lecture fichier terminée");
  setTimeout(() => console.log("E - setTimeout dans I/O callback"), 0);
  setImmediate(() => console.log("F - setImmediate dans I/O callback"));
  // Dans un callback I/O, setImmediate s'exécute TOUJOURS avant setTimeout
});

console.log("G - fin synchrone");
js
// process.nextTick a la priorité la plus haute — s'exécute avant TOUTES les autres microtasks
process.nextTick(() => console.log("nextTick : exécuté en premier après le code synchrone"));

Promise.resolve().then(() => console.log("promise microtask"));

console.log("code synchrone");

// Ordre : "code synchrone", "nextTick...", "promise microtask"
js
// Bloquer l'event loop = tuer la scalabilité — à éviter absolument
function fibonacciBloquant(n) {
  if (n <= 1) return n;
  return fibonacciBloquant(n - 1) + fibonacciBloquant(n - 2); // bloque le thread principal
}

// Mauvais : bloque TOUTES les requêtes en cours pendant le calcul
app.get("/fib/:n", (req, res) => {
  const result = fibonacciBloquant(Number(req.params.n)); // synchrone et lourd = catastrophe
  res.json({ result });
});

// Bon : déléguer à un worker_thread (voir leçon performance)
js
// Mesurer le "lag" de l'event loop — indicateur de santé en production
let lastCheck = Date.now();

setInterval(() => {
  const now = Date.now();
  const drift = now - lastCheck - 1000; // attendu : 1000ms entre deux ticks
  if (drift > 50) {
    console.warn(`Event loop lag détecté : ${drift}ms`);
  }
  lastCheck = now;
}, 1000);

Résumé

  • Node.js est single-threaded pour le JS, mais délègue l'I/O (fichiers, réseau) à libuv en arrière-plan.
  • Les microtasks (Promises, queueMicrotask) se vident intégralement avant chaque macrotask (setTimeout).
  • process.nextTick a une priorité encore supérieure aux Promises.
  • Le calcul CPU-intensif synchrone bloque tout le serveur : à déporter (worker threads, cluster, service externe).

Exercices pratiques

1 disponible
1

Mission : diagnostiquer une API qui gèle pendant 4 secondes

Objectif : Prédire l'ordre d'exécution exact d'un code mêlant nextTick, Promises et setTimeout, puis identifier et corriger le blocage de l'event loop qui fige le serveur entier.

Contexte

L'équipe reçoit un ticket : toutes les 30 secondes environ, l'API Express répond avec 4 secondes de latence sur TOUTES ses routes en même temps, y compris /health qui ne fait rien d'autre que res.json({ ok: true }). En regardant le code du endpoint /api/rapport, tu trouves une fonction genererRapport(donnees) qui fait une triple boucle imbriquée sur un tableau de 50 000 enregistrements, appelée directement dans le handler de route, sans aucun await ni délégation.

Résoudre l’exercice →