Performance de l’API Node.js : un cadre d’optimisation par ordre de priorité
Apprenez à classer les correctifs de performance des API Node.js en fonction du rapport entre l’effort requis et leur impact, afin de résoudre d’abord les problèmes liés au pooling des connexions et aux requêtes N+1 avant de vous attaquer à des optimisations plus complexes.
La plupart des articles sur la vitesse d’une API Node.js vous présentent simplement une liste de quinze conseils, avec par exemple un sérialiseur JSON plus rapide côte à côte avec le pooling des connexions de base de données, comme si chaque élément méritait autant d’attention. C’est trompeur. Certaines corrections ne prennent que dix minutes et réduisent considérablement les temps de réponse. D’autres exigent des mois d’efforts pour un gain négligeable. Comprendre ces différences est bien plus important que de mémoriser chaque élément de la liste.
Niveau 1 : Faites ceci en premier (Fort impact, faible effort)
Ces modifications coûtent presque rien à mettre en œuvre. Elles nécessitent peu de code, présentent peu de risques et, en pratique, elles sont bien plus souvent la véritable cause de la lenteur que les solutions exotiques auxquelles on recourt à la place.
Activez le mode keep-alive pour toutes les requêtes HTTP sortantes. Par défaut, le client HTTP de Node ouvre une nouvelle connexion pour chaque appel, ce qui fait que chaque requête vers un service externe doit à nouveau effectuer l’intégralité de la procédure de handshake TCP ainsi que la négociation TLS. La réutilisation d’un même agent entre les requêtes élimine ces coûts supplémentaires pour chaque appel ultérieur vers le même hôte.
const agent = new https.Agent({ keepAlive: true, maxSockets: 50 });
Communiquez toujours avec votre base de données via un pool de connexions, et non à travers une seule connexion. Sous une charge concurrentielle réelle, une seule connexion devient en fait une file d’attente derrière laquelle tout doit attendre. Un pool de taille appropriée permet à votre API de gérer le trafic concurrentiel tel qu’il est réellement structuré, en parallèle.
Permettez aux appels asynchrones indépendants de s’exécuter en parallèle plutôt qu’un après l’autre. Lorsque deux instructions await ne dépendent pas des résultats de l’autre, il n’y a aucune raison de les forcer à attendre dans l’ordre.
// costs the sum of both calls
const user = await getUser(id);
const orders = await getOrders(id);
// costs roughly the slower of the two
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);
Ajoutez des index pour les colonnes que vos requêtes filtrent, jointent ou trient réellement. Parmi tout ce qui figure sur cette liste, c’est sans doute la mesure la plus efficace à adopter pour tout point de terminaison qui lit dans une table en constante croissance, et il s’agit souvent d’une modification à effectuer en une seule ligne.
Éliminez les schémas de requêtes N+1. Récupérer une liste puis lancer une nouvelle requête pour chaque élément à l’intérieur d’une boucle semble inoffensif avec quelques enregistrements de test, mais devient un problème sérieux lorsqu’on traite des milliers d’enregistrements réels. Remplacez la boucle par une seule requête en lot pour les données associées.
Limitez chaque requête qui pourrait sinon retourner un ensemble de résultats illimité avec un LIMIT. Une interface qui renvoie « tous les commandes que ce client a jamais passées » sans limite fonctionne bien pour un compte tout neuf, mais échoue complètement pour un compte ayant des années d’historique de commandes.
Niveau 2 : Digne d’un véritable investissement (fort impact, effort important)
Les solutions de ce niveau ne consistent pas en de petites modifications. Elles exigent un travail de conception réel, et souvent une nouvelle infrastructure, mais elles résolvent des catégories de problèmes que aucune solution de niveau 1 ne peut traiter.
Mettez en place une couche de mise en cache pour les lectures coûteuses et fréquentes. Placer Redis devant une requête d’agrégation lente ou une appel coûteux à une API tierce peut réduire un temps de réponse de 200 ms à environ 2 ms. La partie difficile n’est pas la mise en place de la cache, mais la conception d’une stratégie d’invalidation suffisamment solide pour que la cache ne fournisse jamais de résultats obsolètes.
Retirez complètement les tâches lentes et non critiques du cycle demande-réponse. L’envoi d’un e-mail de confirmation, la création d’un rapport ou la mise à jour des données analytiques n’ont pas besoin d’être terminées avant que vous ne répondiez au client. En associant une file d’attente, telle que BullMQ ou SQS, à un processus travailleur dédié, on peut réduire une requête qui prenait auparavant 2 secondes à environ 80 millisecondes.
Passez d’une pagination basée sur l’offset à une pagination basée sur un curseur pour des ensembles de résultats volumineux ou profonds. La pagination basée sur OFFSET ralentit progressivement à mesure que l’on avance dans les pages, car la base de données doit encore parcourir toutes les lignes précédentes. Une approche basée sur un curseur coûte à peu près autant, que l’on se trouve à la page 5 ou à la page 5 000.
Étendez horizontalement avec un véritable équilibreur de charge et un état de session ou de cache centralisé. Peu importe à quel point vous l’avez optimisée, une seule instance Node atteint finalement ses limites. En exécutant plusieurs instances derrière un équilibreur de charge, chacune partageant un cache Redis et un pool de connexions communs, on élève ces limites sans avoir à rendre chaque processus individuellement plus rapide.
Profil avant toute optimisation supplémentaire. Une fois les problèmes évidents résolus, deviner ce qui ralentit le système cesse d’être une stratégie fiable. L’utilisation d’un véritable outil de profilage ou d’APM, comme clinic.js, un produit APM hébergé, ou même l’exécution de EXPLAIN ANALYZE sur une requête suspecte, vous montre où le temps est réellement consacré, plutôt que là où vous supposez qu’il doit l’être.
Niveau 3 : Généralement pas digne de priorité (faible impact, souvent surévalué)
Ces solutions reviennent constamment dans les discussions sur les performances, mais elles ont rarement un effet significatif sur une API en production réelle, principalement parce qu’elles ciblent des parties de la pile technique qui n’étaient jamais le véritable goulot d’étranglement à l’origine.
Ajustement fin de la sérialisation JSON. Il existe bien des bibliothèques JSON plus rapides qui peuvent être utiles, mais uniquement à une échelle que la grande majorité des API ne atteint jamais. Si votre véritable problème est une requête de base de données prenant 300 ms, gagner quelques millisecondes sur la sérialisation ne résout pas le bon problème.
Changer de framework uniquement pour réduire la charge supplémentaire qu’il représente. L’écart de performance entre Express et une alternative prétendument plus rapide est réel, mais il reste faible par rapport aux coûts engendrés par une table non indexée ou des requêtes N+1. Le choix du framework peut avoir de l’importance pour de nombreuses autres raisons ; la vitesse brute n’en fait généralement pas partie.
Essayer d’abord le regroupement des données avant de s’occuper de tout le reste. Lancer plusieurs processus Node pour exploiter des cœurs CPU supplémentaires aide réellement les tâches fortement gourmandes en ressources CPU. Cela ne sert à rien pour un point d’entrée lent parce qu’il est bloqué en attente d’une requête non indexée, car l’attente reste de l’attente quel que soit le nombre de processus inactifs qui attendent également.
Réécrire les chemins de code les plus sollicités dans un langage de niveau inférieur uniquement pour améliorer les performances. Il existe des cas légitimes pour cela, lorsque un goulot d’étranglement spécifique et avéré lié au CPU le justifie. Cependant, bien plus souvent, cette démarche est tentée avant même que quiconque n’ait confirmé que c’est bien là que se perdent les temps, transformant ainsi une technique efficace en un effort inutile dirigé vers le mauvais objectif.
Comment l’utiliser réellement
Videz d’abord tout le niveau 1 avant d’envisager quoi que ce soit d’autre, car il est peu coûteux, à faible risque et permet de résoudre la plupart des problèmes de performance dans le monde réel. Ne passez au niveau 2 que pour les points spécifiques où l’analyse montre que le niveau 1 n’était pas suffisant, et non en considérant cela comme une réécriture à appliquer partout. Laissez le niveau 3 intact jusqu’à ce que vous ayez des preuves concrètes, provenant d’un outil d’analyse et non de simples suppositions, indiquant que l’une de ces techniques spécifiques constitue réellement le goulot d’étranglement. La plupart des API qui semblent lentes le sont en raison d’un problème non résolu du niveau 1, et non parce qu’elles manquent d’une optimisation obscure tirée d’un article de blog.
Lectures complémentaires
- Express vs Fastify en 2026 : Un comparatif pratique des frameworks Node.js — Ce guide compare Express et Fastify en termes de performance, de validation, d’écosystème et de gestion des erreurs, et aborde également les principales modifications majeures d’Express 5.
- Node.js, Deno et Bun comparés : Essais de performance, compromis et stratégie de migration — Il explique les véritables différences architecturales entre Node.js, Deno et Bun, ce que révèlent les essais de performance de 2025, et comment décider s’il faut migrer et quand le faire.