backend / nodejs
Debugging et profiling
Explication
Ce que vous allez apprendre
- Déboguer une application Node.js en direct avec
node --inspectet Chrome DevTools - Mesurer précisément la durée d'une opération avec
console.time/console.timeEnd - Identifier le vrai goulot d'étranglement CPU avec le profiling, plutôt qu'une intuition
- Détecter une fuite mémoire en comparant des heap snapshots pris à différents moments
- Reproduire un problème de performance dans un environnement contrôlé plutôt qu'en production
Dans quel contexte ?
Après plusieurs jours de fonctionnement, un serveur Node.js en production consomme de plus en plus de mémoire jusqu'à planter avec une erreur "Out Of Memory", forçant un redémarrage manuel toutes les 48 heures. Personne ne sait quelle partie du code en est responsable. Cette leçon montre les outils précis pour transformer cette investigation à l'aveugle en un diagnostic mesurable et reproductible.
Le point de départ : au-delà du console.log
console.log un peu partout dans le code est souvent le premier réflexe de débogage. Mais il devient vite insuffisant : difficile à retirer proprement, il ne montre pas la durée d'une opération.
Il ne permet pas non plus d'explorer l'état de la mémoire ou d'identifier quelle fonction précisément ralentit l'application. Cette leçon présente des outils plus adaptés à ces problèmes plus complexes.
Commençons par le débogueur interactif, pour voir l'exécution en direct. node --inspect connecte l'application aux Chrome DevTools.
Ça permet de poser des points d'arrêt, d'inspecter des variables en temps réel, et d'avancer ligne par ligne dans l'exécution. Bien plus puissant que d'ajouter et retirer des console.log à chaque hypothèse de bug.
| Outil | Répond à la question |
|---|---|
node --inspect + DevTools | Que fait ce code, ligne par ligne, en direct ? |
| Profiling CPU | Quelle fonction consomme le plus de temps CPU ? |
| Heap snapshots | Quels objets s'accumulent anormalement en mémoire ? |
Une fois qu'on sait inspecter le code en direct, une autre question se pose : qui ralentit vraiment l'application ? Une intuition sur ce qui est lent est souvent fausse.
Le profiling CPU enregistre précisément le temps passé dans chaque fonction pendant l'exécution. Il révèle objectivement le vrai goulot d'étranglement, parfois une fonction anodine appelée des milliers de fois plutôt que le calcul "évidemment lourd" auquel on pensait.
Il reste un type de problème différent et plus sournois à connaître : les fuites mémoire. Une fuite ne fait pas planter l'application immédiatement, elle grossit progressivement.
Le crash (Out Of Memory) peut survenir des jours après le déploiement. Ça rend la fuite difficile à associer à sa cause réelle sans outil dédié.
Comparer des heap snapshots pris à différents moments permet de voir quels types d'objets s'ACCUMULENT anormalement. Des écouteurs d'événements jamais retirés, ou un cache qui grossit sans limite, par exemple.
Le piège fréquent à connaître avant de pratiquer : faire du profiling ou chercher une fuite mémoire directement en production peut lui-même dégrader les performances pendant l'investigation. Il vaut mieux reproduire le problème dans un environnement contrôlé et surveiller en continu, plutôt que d'attendre un incident pour commencer à chercher.
Piège fréquent
Lancer un profiling CPU complet ou prendre des heap snapshots directement sur le serveur de production, sous charge réelle, peut lui-même ralentir l'application au pire moment. Privilégiez un environnement de staging qui reproduit fidèlement la charge, ou des outils de monitoring continu (APM) conçus pour un coût minimal en production.
Commandes & code
Debugging et profiling
# Débogueur intégré via Chrome DevTools
node --inspect src/index.js
# ou pour s'arrêter dès la première ligne :
node --inspect-brk src/index.js
# puis ouvrir chrome://inspect dans Chrome// console avancé — au-delà de console.log
console.table([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]);
console.time("requête-db");
await db.query("SELECT * FROM users");
console.timeEnd("requête-db"); // affiche la durée écoulée
console.group("Traitement de la commande #42");
console.log("Validation des articles...");
console.log("Calcul du total...");
console.groupEnd();
console.trace("Point d'exécution atteint"); // affiche la stack trace complète// Profiling CPU — génère un fichier .cpuprofile analysable dans Chrome DevTools
// node --prof src/index.js
// puis : node --prof-process isolate-0x*.log > profile.txt
import { Session } from "node:inspector/promises";
import { writeFile } from "node:fs/promises";
async function profileCpu(durationMs, fn) {
const session = new Session();
session.connect();
await session.post("Profiler.enable");
await session.post("Profiler.start");
await fn();
await new Promise((r) => setTimeout(r, durationMs));
const { profile } = await session.post("Profiler.stop");
await writeFile("./cpu-profile.cpuprofile", JSON.stringify(profile));
session.disconnect();
}// Détecter une fuite mémoire — snapshot du heap à comparer dans le temps
import v8 from "node:v8";
import { writeFileSync } from "node:fs";
function takeHeapSnapshot(label) {
const snapshotStream = v8.getHeapSnapshot();
const filename = `heap-${label}-${Date.now()}.heapsnapshot`;
const chunks = [];
snapshotStream.on("data", (chunk) => chunks.push(chunk));
snapshotStream.on("end", () => {
writeFileSync(filename, Buffer.concat(chunks));
console.log(`Snapshot écrit : ${filename}`);
});
}
// Comparer deux snapshots pris à intervalles dans Chrome DevTools > Memory
setInterval(() => takeHeapSnapshot("periodic"), 60 * 60 * 1000);// Surveiller l'usage mémoire en continu — alerte simple avant un OOM
setInterval(() => {
const usage = process.memoryUsage();
const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);
const rssMB = Math.round(usage.rss / 1024 / 1024);
if (heapUsedMB > 500) {
console.warn(`Utilisation mémoire élevée : heap=${heapUsedMB}MB rss=${rssMB}MB`);
}
}, 30_000);// launch.json VS Code — débogage intégré à l'éditeur
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug API",
"program": "${workspaceFolder}/src/index.js",
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal"
}
]
}Résumé
node --inspectconnecte Chrome DevTools pour un débogage interactif complet (breakpoints, watch).console.time/console.table/console.groupstructurent des logs de debug bien plus lisibles queconsole.logbrut.- Le profiling CPU (
--profou l'APIinspector) identifie les fonctions qui consomment le plus de temps. - Des heap snapshots comparés dans le temps révèlent les fuites mémoire (objets qui s'accumulent sans être libérés).
Exercices pratiques
Mission : un serveur qui plante toutes les 48 heures sans coupable évident
Objectif : Diagnostiquer méthodiquement une fuite mémoire en production, distinguer une vraie fuite d'une simple charge élevée, et localiser le composant fautif via des heap snapshots comparés.
Contexte
Le serveur redémarre automatiquement toutes les 48 heures avec une erreur "JavaScript heap out of memory". Le graphique de process.memoryUsage().heapUsed, relevé toutes les 30 secondes, montre une courbe qui monte de façon quasiment linéaire, sans jamais redescendre, même la nuit quand le trafic tombe à presque zéro. Un développeur soupçonne un setInterval qui accumule des écouteurs d'événements sur un EventEmitter partagé, ajouté à chaque nouvelle connexion WebSocket sans jamais être retiré à la déconnexion.