Retour au cours

backend / nodejs

Debugging et profiling

Leçon 171 exercice

Explication

Ce que vous allez apprendre

  • Déboguer une application Node.js en direct avec node --inspect et 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.

OutilRépond à la question
node --inspect + DevToolsQue fait ce code, ligne par ligne, en direct ?
Profiling CPUQuelle fonction consomme le plus de temps CPU ?
Heap snapshotsQuels 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

bash
# 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
js
// 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
js
// 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();
}
js
// 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);
js
// 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);
json
// 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 --inspect connecte Chrome DevTools pour un débogage interactif complet (breakpoints, watch).
  • console.time/console.table/console.group structurent des logs de debug bien plus lisibles que console.log brut.
  • Le profiling CPU (--prof ou l'API inspector) 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

1 disponible
1

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.

Résoudre l’exercice →