Bonjour ! Ravi de vous retrouver pour continuer notre exploration de Node.js.
Dans la leçon précédente, nous avons acquis une syntaxe JavaScript moderne et puissante avec les fonctions fléchées, la déstructuration et les opérateurs spread/rest. Ces outils sont les briques de base pour écrire du code asynchrone clair et efficace.
Aujourd'hui, nous abordons le concept le plus fondamental et le plus déterminant de l'architecture Node.js : le modèle d'exécution asynchrone et la boucle d'événements (Event Loop). Comprendre ce mécanisme est la clé pour concevoir des applications backend performantes et scalables, l'un de vos objectifs principaux. Pour un développeur venant d'un environnement comme Laravel/PHP où le modèle de requête est souvent synchrone (une requête arrive, le code s'exécute de haut en bas, une réponse est envoyée), cette leçon représente un changement de paradigme essentiel.
À la fin de cette leçon, vous serez en mesure d'expliquer précisément comment Node.js peut gérer des milliers de connexions simultanées avec un seul thread, en démystifiant les termes "asynchrone", "non-bloquant" et "Event Loop".
1. Le problème : pourquoi l'asynchrone est-il nécessaire ?
Avant Node.js, la plupart des serveurs web suivaient un modèle simple mais coûteux : un thread (un processus d'exécution) par requête. Si une requête devait attendre une opération lente (lire un fichier, interroger une base de données), le thread entier était bloqué, consommant des ressources sans rien faire.
Pour comprendre la solution apportée par Node.js, je vous propose de lire les extraits d'un article qui illustre parfaitement ce problème et la solution élégante de Node.js.
What Node.js Actually Is - NodeBook
L'article 'What Node.js Actually Is' de NodeBook explique l'origine de Node.js. Vous y découvrirez l'analogie du café pour comparer le modèle bloquant traditionnel au modèle non-bloquant de Node.js.
Lisez les sections intitulées 'Why Blocking I/O Was a Party Everyone Hated' et 'The "Aha!" Moment'. Concentrez-vous sur la description du modèle 'thread-per-request' et comment l'analogie du barista illustre l'efficacité du modèle événementiel.
La philosophie de Node.js est donc radicalement différente : ne jamais attendre. Au lieu d'un barista dédié à chaque client du début à la fin, Node.js agit comme un unique coordinateur ultra-efficace qui prend les commandes, les délègue à des "machines" (le système d'exploitation, un pool de threads), et revient servir les clients dès que leur commande est prête. Ce coordinateur, c'est l'Event Loop.
Démonstration pratique : Bloquant vs. Non-bloquant
La théorie est une chose, mais une démonstration concrète est bien plus parlante. L'article que vous venez de lire propose une expérience simple mais très révélatrice.
Le serveur bloquant (Python)
Ce serveur simule une tâche lente avec time.sleep(2). Cette fonction bloque tout le processus.
# blocking_server.py
import time
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
print("Request received. Starting 2-second 'work'...")
time.sleep(2) # BLOCKS the entire process!
print("Work finished. Sending response.")
return "Hello from the blocking server!"
if __name__ == '__main__':
app.run(port=5000)
Le serveur non-bloquant (Node.js)
Ce serveur utilise setTimeout, qui est non-bloquant. Il planifie une tâche et rend la main immédiatement.
// non_blocking_server.js
import http from "http";
const server = http.createServer((req, res) => {
console.log("Request received. Scheduling 2-second 'work'...");
// NON-BLOCKING: schedules work and returns immediately.
setTimeout(() => {
console.log("Work finished. Sending response.");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from the non-blocking server!");
}, 2000);
});
server.listen(5001, () => {
console.log("Server running on http://localhost:5001/");
});
Si nous envoyons 10 requêtes en même temps à ces deux serveurs :
- Serveur bloquant : Il traitera les requêtes l'une après l'autre. Le temps total sera d'environ 20 secondes (10 requêtes * 2 secondes).
- Serveur non-bloquant : Il acceptera les 10 requêtes quasi instantanément, lancera les 10 minuteurs en parallèle, et répondra à toutes les requêtes après environ 2 secondes.
Cette différence de performance fondamentale est la raison d'être de Node.js.
2. Le mécanisme : les acteurs de l'asynchronie
Maintenant que nous avons vu le "pourquoi", explorons le "comment". Comment JavaScript, qui est fondamentalement mono-thread (single-threaded), peut-il accomplir cela ?
La magie opère grâce à la collaboration de plusieurs composants. Une vidéo animée est parfaite pour visualiser ce flux.
JavaScript Event Loop: How it Works and Why it Matters in 5 Minutes
Cette courte vidéo de James Q Quick décompose de manière très claire le fonctionnement de la boucle d'événements. Elle vous présentera les concepts de 'Call Stack', 'libuv', et 'Event Queue'.
Regardez la vidéo de 01:19 à 06:47. Observez attentivement le parcours d'une fonction synchrone par rapport à une fonction asynchrone et le rôle de chaque composant.
Résumons le flux que vous venez de voir :
- Call Stack (Pile d'appels) : C'est là que JavaScript garde une trace des fonctions en cours d'exécution. C'est une structure de données LIFO (Last-In, First-Out).
- Node.js APIs / libuv : Quand une fonction asynchrone (ex:
fs.readFile,setTimeout, une requête à une base de données) est appelée, elle n'est pas exécutée directement par JavaScript. Elle est transmise aux APIs de Node.js, qui s'appuient sur une bibliothèque C++ nomméelibuv. C'estlibuvqui gère ces opérations en arrière-plan, souvent en utilisant les capacités multi-threads du système d'exploitation ou son propre pool de threads. Votre code JavaScript principal n'est donc pas bloqué. - Callback Queue (File d'attente des callbacks) / Task Queue : Une fois que l'opération asynchrone est terminée, la fonction de callback qui lui est associée n'est pas exécutée immédiatement. Elle est placée dans une file d'attente.
- Event Loop (Boucle d'événements) : C'est le chef d'orchestre. Son rôle est simple mais crucial : il surveille en permanence la Call Stack et la Callback Queue. Si la Call Stack est vide, il prend le premier callback de la file et le pousse sur la pile pour exécution.
Ce cycle se répète indéfiniment, d'où le nom de "boucle".

3. Un niveau de profondeur : Microtasks et Macrotasks
Le modèle que nous venons de voir est légèrement simplifié. En réalité, il n'y a pas une, mais deux files d'attente principales qui nous intéressent :
- Task Queue (ou Macrotask Queue) : Pour les callbacks de
setTimeout,setInterval, les opérations I/O (lecture de fichier, requêtes réseau), etsetImmediate. - Microtask Queue (ou Job Queue) : Pour les callbacks des Promises (
.then,.catch,.finally) etprocess.nextTick().
Pourquoi cette distinction ? Parce qu'elles n'ont pas la même priorité.
La règle d'or de l'Event Loop :
Après chaque tâche exécutée depuis la Task Queue, l'Event Loop exécute TOUTES les tâches présentes dans la Microtask Queue avant de passer à la tâche suivante de la Task Queue. La Microtask Queue a donc une priorité plus élevée.
La vidéo suivante visualise très bien cette priorité.
JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
Lydia Hallie propose une excellente visualisation qui fait la distinction entre la Task Queue et la Microtask Queue. Bien que ses exemples utilisent des 'Web APIs' (du navigateur), le principe est identique pour les 'Node.js APIs'.
Regardez de 06:05 à 12:15. Concentrez-vous sur : La définition de la Microtask Queue et pour quels types d'opérations elle est utilisée (les Promises sont le cas le plus important pour nous). La règle de priorité de la Microtask Queue sur la Task Queue. Le quiz à la fin est un excellent exercice pour tester votre compréhension. Essayez de prédire le résultat avant de voir la solution.
Cette distinction est cruciale car async/await, que nous verrons bientôt, est basé sur les Promises. Comprendre que leurs opérations passent par la Microtask Queue vous aidera à déboguer des comportements asynchrones complexes.

4. Les phases de l'Event Loop de Node.js
Pour une compréhension exhaustive, utile notamment pour des entretiens techniques, il faut savoir que l'Event Loop de Node.js est officiellement décomposé en plusieurs phases distinctes qui s'exécutent dans un ordre précis à chaque "tick" de la boucle.
La documentation officielle de Node.js fournit le schéma le plus précis des phases de l'Event Loop. Nous n'allons pas tout lire en détail, mais nous allons en extraire la structure.
Consultez la section 'Event Loop Explained' pour voir le diagramme ASCII des phases. Ensuite, parcourez rapidement la section 'Phases in Detail' pour avoir une idée de ce que chaque phase représente. Ne vous attardez pas sur les détails, concentrez-vous sur le nom des phases principales.
Voici un résumé simplifié des phases les plus importantes :
- timers : Exécute les callbacks planifiés par
setTimeout()etsetInterval(). - pending callbacks : Exécute des callbacks I/O qui ont été différés au tick précédent (cas rares).
- idle, prepare : Phase interne à Node.js.
- poll : La phase la plus importante. Elle récupère les nouveaux événements I/O (lecture de fichier terminée, nouvelle requête HTTP, etc.) et exécute leurs callbacks. Si la file
pollest vide, la boucle attend ici ou passe à la phasechecksi dessetImmediatesont planifiés. - check : Exécute les callbacks planifiés par
setImmediate(). - close callbacks : Gère les événements de fermeture (ex:
socket.on('close', ...)).
Et où sont process.nextTick() et les Promises ? Ils ne font pas partie de ces phases. Leurs callbacks sont exécutés immédiatement après l'opération en cours, avant que l'Event Loop ne passe à la phase suivante. C'est pour cela qu'ils ont une priorité encore plus élevée que les callbacks de setImmediate dans la phase check.
Conclusion
Félicitations pour avoir abordé ce sujet complexe mais fondamental. Une bonne compréhension du modèle asynchrone de Node.js vous distingue en tant que développeur backend compétent.
Points clés à retenir :
- Node.js est asynchrone et non-bloquant : Il utilise un seul thread pour exécuter votre code JavaScript, il ne faut donc jamais le bloquer avec des opérations longues et synchrones.
- L'Event Loop est le chef d'orchestre : Il permet à Node.js de déléguer des tâches lentes à
libuv(qui peut utiliser d'autres threads) et d'exécuter les callbacks correspondants lorsque les tâches sont terminées et que la pile d'appels est vide. - Deux types de files d'attente : La Microtask Queue (pour les Promises) est prioritaire et est entièrement vidée après chaque tâche de la Task Queue (pour
setTimeout, I/O). - Le modèle est une collaboration : V8 (moteur JS), libuv (gestion de l'asynchrone), les APIs Node.js et l'Event Loop travaillent tous ensemble pour créer ce modèle performant.
Dans nos prochaines leçons, nous allons arrêter de nous concentrer sur le comment ça marche pour nous focaliser sur le comment l'utiliser. Nous commencerons par maîtriser les Promises, le mécanisme standard pour gérer les opérations asynchrones en JavaScript moderne.