← Retour au blog
Concurrence

Blocking vs Non-Blocking : maîtriser les performances de votre Event Loop

Comprendre Blocking et Non-Blocking dans Node.js : Reactor Pattern, Event Loop, thread pools et l'importance du monitoring pour éviter de dégrader les performances.

📅 ✍️ Antoine Coulon
nodejsevent-loopblockingnon-blockingreactor-pattern

Quelques lignes de code suffisent parfois à ralentir toute une application et à dégrader l’expérience de vos utilisateurs. Pas à cause d’un algorithme exotique ou d’une fuite mémoire difficile à traquer, mais simplement parce que la frontière entre opérations Blocking et Non-Blocking n’a pas été respectée. C’est l’une des distinctions les plus fondamentales du développement back-end, et l’une des plus mal comprises.

Selon les plateformes d’exécution que vous utilisez, vous serez confronté au modèle Blocking, au modèle Non-Blocking, ou aux deux à la fois. Node.js est un terrain d’observation idéal : son architecture met précisément en lumière l’usage conjoint de ces deux concepts, et c’est ce qui en fait un excellent support pour les comprendre.

Blocking : un thread monopolisé

Une opération Blocking monopolise l’entièreté du thread sur lequel elle s’exécute, jusqu’à son terme. Tant qu’elle n’est pas terminée, plus rien d’autre ne peut s’exécuter sur ce thread : il est entièrement accaparé.

Deux grandes familles d’opérations sont typiquement bloquantes :

Le piège, avec Node.js, tient à son modèle d’exécution. Même si la plateforme est multithreaded sous le capot, un seul thread est par défaut mobilisé pour exécuter votre programme. Bloquer ce thread principal, c’est empêcher toute autre opération d’avancer, et donc provoquer un ralentissement global de l’application, y compris pour les requêtes qui n’ont aucun rapport avec l’opération coûteuse en cours.

On pourrait croire qu’une thread pool règle le problème en répartissant la charge sur plusieurs threads. C’est en partie vrai, mais ce n’est pas une solution miracle : avec ou sans Node.js, une thread pool atteint vite la saturation si elle est mal dimensionnée. D’où l’importance de calibrer la taille de ces pools en fonction de la charge réelle et de les monitorer en continu, sous peine de transformer la pool elle-même en goulot d’étranglement.

Non-Blocking : un thread qui ne s’arrête jamais

À l’inverse, une opération Non-Blocking ne bloque pas le thread : elle permet au reste du programme de poursuivre son exécution pendant qu’elle se déroule. Le thread n’est pas mis en attente, il reste disponible pour traiter d’autres travaux.

Cette nature Non-Blocking est rendue possible par une approche Event-Driven. Plutôt que d’attendre activement la fin de l’opération, on la délègue en arrière-plan ; un événement est ensuite déclenché une fois celle-ci terminée, et le code de suite (le callback, la résolution de la promesse) est alors planifié. C’est le fameux Reactor Pattern, dont l’Event Loop de Node.js est une implémentation concrète.

Le bénéfice est direct : un unique thread peut traiter plusieurs opérations de manière concurrente, au lieu d’être assigné à l’exécution d’une seule tâche du début à la fin. C’est précisément ce qui permet à Node.js de gérer un grand nombre de connexions simultanées sans avoir besoin d’un thread par requête.

Garder l’Event Loop réactive

Pour tirer pleinement parti de ce modèle, il faut respecter une règle d’or : l’Event Loop doit rester réactive et ne jamais être ralentie ou bloquée par du code Blocking. Une seule opération synchrone trop longue suffit à figer l’ensemble du traitement concurrent : c’est le retour direct au problème de la section précédente.

Deux leviers permettent de préserver cette réactivité :

L’importance du monitoring

Comprendre la théorie ne suffit pas : encore faut-il vérifier, en production, que votre runtime se comporte comme prévu. Il est primordial de monitorer l’état de santé de votre environnement d’exécution, et deux catégories de métriques méritent une attention particulière :

Pour Node.js, l’écosystème propose des outils de diagnostic dédiés. Clinic.js, par exemple, mesure ces métriques vitales (heap, CPU, event loop lag et ELU) et aide à identifier visuellement les points de blocage.

Une astuce complémentaire, plus chirurgicale : le flag --trace-sync-io permet de repérer certaines opérations d’I/O synchrones (donc bloquantes) au moment où elles surviennent. Un excellent moyen de débusquer un *Sync oublié dans une portion de code censée être asynchrone.

▶ La démonstration en vidéo est disponible sur le post LinkedIn original.

Conclusion

Blocking et Non-Blocking ne sont pas deux camps opposés entre lesquels il faudrait choisir : ce sont deux outils complémentaires, dont la bonne articulation conditionne les performances de votre application. Le modèle Non-Blocking de Node.js offre une concurrence remarquable sur un seul thread, mais cette concurrence n’a de valeur que si l’Event Loop reste libre de battre à son rythme.

La discipline tient en quelques principes : privilégier les APIs asynchrones, isoler les traitements lourds dans des threads séparés, et surtout instrumenter votre runtime pour mesurer ce qui se passe réellement. Car en matière de performances, l’intuition trompe souvent : seul le monitoring dit la vérité.