Mémoire et performance JS/TS - 10 - Profiling mémoire en Node.js

Heap snapshots en Node.js, process.memoryUsage(), clinic.js, flamegraphs et tuning du heap V8.

  1. 01 Mémoire et performance JS/TS - 00 - Pourquoi la mémoire compte meme avec un garbage collector
  2. 02 Mémoire et performance JS/TS - 01 - Stack vs Heap
  3. 03 Mémoire et performance JS/TS - 02 - Le cycle de vie de la mémoire
  4. 04 Mémoire et performance JS/TS - 03 - Le garbage collector
  5. 05 Mémoire et performance JS/TS - 04 - V8 en profondeur
  6. 06 Mémoire et performance JS/TS - 05 - Les 6 fuites mémoire classiques
  7. 07 Mémoire et performance JS/TS - 06 - Closures et mémoire
  8. 08 Mémoire et performance JS/TS - 07 - WeakRef, WeakMap et WeakSet
  9. 09 Mémoire et performance JS/TS - 08 - FinalizationRegistry : savoir quand le GC passe
  10. 10 Mémoire et performance JS/TS - 09 - DevTools Memory : investiguer dans Chrome
  11. 11 Mémoire et performance JS/TS - 10 - Profiling mémoire en Node.js
  12. 12 Mémoire et performance JS/TS - 11 - Détecter et corriger les fuites mémoire
  13. 13 Mémoire et performance JS/TS - 12 - ArrayBuffer et TypedArrays
  14. 14 Mémoire et performance JS/TS - 13 - Workers et mémoire partagee
  15. 15 Mémoire et performance JS/TS - 14 - Streams et backpressure
  16. 16 Mémoire et performance JS/TS - 15 - Fuites mémoire en React
  17. 17 Mémoire et performance JS/TS - 16 - Serveurs Node.js et mémoire
  18. 18 Mémoire et performance JS/TS - 17 - Mémoire et Docker
  19. 19 Mémoire et performance JS/TS - 18 - Optimisations mémoire
  20. 20 Mémoire et performance JS/TS - 19 - Comparaison avec d'autres langages
  21. 21 Mémoire et performance JS/TS - 20 - Tester la mémoire
  22. 22 Mémoire et performance JS/TS - 21 - Glossaire

10 - Profiling mémoire en Node.js

Ce que tu vas apprendre

  • Connecter Chrome DevTools a un process Node.js avec --inspect
  • Generer des heap snapshots depuis le code avec v8.writeHeapSnapshot()
  • Lire process.memoryUsage() (rss, heapTotal, heapUsed, external, arrayBuffers)
  • Utiliser clinic.js et 0x pour des analyses avancees
  • Tuner --max-old-space-size et surveiller la mémoire en production

Prerequisites

Avoir lu l'article 09 sur Chrome DevTools Memory.


Chrome DevTools, c'est genial pour le navigateur. Mais quand ta fuite mémoire est cote serveur, dans un process Node.js qui tourne depuis 3 jours en production et qui mange 2 Go de RAM, il te faut d'autres outils. La bonne nouvelle : la plupart sont deja intégrés a Node.

--inspect : connecter DevTools a Node

La méthode la plus directe. Lance ton serveur avec le flag --inspect :

bashnode --inspect dist/server.js

Node affiche une URL du type ws://127.0.0.1:9229/xxxxx. Ouvre chrome://inspect dans Chrome, ton process apparaît. Clique "inspect" et tu retrouves les memes outils Memory que dans le navigateur.

Pour attacher l'inspecteur a un process deja en cours :

bash# Envoyer SIGUSR1 au process (Linux/Mac)
kill -SIGUSR1 <pid>

Avec Bun, c'est similaire :

bashbun --inspect dist/server.ts

Heap snapshots depuis le code

Tu ne veux pas toujours attacher un debugger. Parfois tu veux générer un snapshot programmatiquement, par exemple toutes les heures ou quand la mémoire dépassé un seuil :

typescriptimport v8 from "node:v8";
import process from "node:process";

function takeSnapshotIfNeeded(): void {
  const usage = process.memoryUsage();
  const heapUsedMB = usage.heapUsed / 1024 / 1024;

  if (heapUsedMB > 500) {
    const filename = v8.writeHeapSnapshot();
    console.log(`Heap snapshot ecrit dans : ${filename}`);
  }
}

// Verifier toutes les 5 minutes
setInterval(takeSnapshotIfNeeded, 5 * 60 * 1000);

Le fichier .heapsnapshot généré se charge dans Chrome DevTools (onglet Memory, "Load"). Tu peux le comparer avec un snapshot pris plus tot.

process.memoryUsage()

C'est ta première ligne de diagnostic. Cinq valeurs, chacune avec une signification precise :

typescriptconst usage = process.memoryUsage();
console.log({
  rss: `${(usage.rss / 1024 / 1024).toFixed(1)} MB`,
  heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(1)} MB`,
  heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(1)} MB`,
  external: `${(usage.external / 1024 / 1024).toFixed(1)} MB`,
  arrayBuffers: `${(usage.arrayBuffers / 1024 / 1024).toFixed(1)} MB`,
});
  • rss (Resident Set Size) : la mémoire totale du process vue par l'OS. Inclut le code, la stack, le heap, les bibliothèques partagees.
  • heapTotal : la taille totale du heap V8 (alloue mais pas forcement utilise).
  • heapUsed : la partie du heap V8 effectivement utilisee par les objets JS.
  • external : la mémoire allouee par les bindings C++ de Node (Buffers, etc.).
  • arrayBuffers : la mémoire des ArrayBuffer et SharedArrayBuffer.

Si heapUsed monte en continu, tu as une fuite dans le code JS. Si external monte, c'est un Buffer ou un binding natif qui fuit. Si rss monte mais pas heapUsed, c'est de la mémoire native (peut-etre un addon C++).

Un endpoint de monitoring

En dev, j'ajoute souvent un endpoint /debug/memory :

typescriptapp.get("/debug/memory", (req, res) => {
  const usage = process.memoryUsage();
  res.json({
    rss: Math.round(usage.rss / 1024 / 1024),
    heapTotal: Math.round(usage.heapTotal / 1024 / 1024),
    heapUsed: Math.round(usage.heapUsed / 1024 / 1024),
    external: Math.round(usage.external / 1024 / 1024),
    uptime: Math.round(process.uptime()),
  });
});

Appeler cet endpoint régulièrement et grapher les résultats, c'est le moyen le plus simple de voir si la mémoire derive.

clinic.js

clinic.js est une suite d'outils de diagnostic par NearForm. Trois outils principaux :

bash# doctor : vue d'ensemble (CPU, memoire, event loop, handles)
npx clinic doctor -- node dist/server.js

# heapprofile : profiling du heap (quelles fonctions allouent le plus)
npx clinic heapprofiler -- node dist/server.js

# bubbleprof : visualisation de l'activite async
npx clinic bubbleprof -- node dist/server.js

clinic doctor généré un rapport HTML avec des diagnostics automatiques. Il te dit si le problème est la mémoire, le CPU ou l'event loop. C'est un bon point de depart quand tu ne sais pas ou chercher.

clinic heapprofiler est plus cible : il montre quelles fonctions allouent le plus de mémoire. Tres utile pour les fuites lentes.

0x pour les flamegraphs

0x généré des flamegraphs CPU. Pas directement lie a la mémoire, mais utile pour comprendre ou le CPU passe son temps (ce qui impacte souvent la mémoire via des allocations excessives) :

bashnpx 0x dist/server.js

Le flamegraph montre la stack d'appels. Les fonctions larges en haut du graphe sont celles qui consomment le plus de CPU. Si tu vois une fonction d'allocation dominer, tu as trouve ton problème.

--max-old-space-size

Par défaut, V8 limite le heap a environ 1.5-2 Go (selon la version et l'OS). Si ton application a besoin de plus :

bashnode --max-old-space-size=4096 dist/server.js

La valeur est en Mo. Mais attention : augmenter la limite ne resout pas une fuite mémoire, ca retarde juste le crash. Le GC mettra aussi plus de temps a scanner un heap de 4 Go.

Pour les applications qui traitent beaucoup de donnees en mémoire (caches, traitements batch), c'est parfois nécessaire. Pour un serveur web classique, si tu depasses 1 Go de heap, tu as probablement une fuite.

Monitoring en production

En production, tu ne veux pas attacher un debugger. Tu veux des metriques exportees vers Prometheus, Datadog, ou équivalent.

typescriptimport { collectDefaultMetrics, Gauge, register } from "prom-client";

collectDefaultMetrics();

const heapUsedGauge = new Gauge({
  name: "nodejs_custom_heap_used_bytes",
  help: "Heap used in bytes",
});

setInterval(() => {
  const usage = process.memoryUsage();
  heapUsedGauge.set(usage.heapUsed);
}, 10000);

// Endpoint pour Prometheus
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});

Les alertes a configurer :

  • heapUsed > 80% de max-old-space-size : warning
  • heapUsed en croissance monotone sur 1h : fuite probable
  • rss > 2x heapTotal : fuite de mémoire native

Sur paltemps.fr, je surveille heapUsed et rss avec des alertes sur la derivee (taux de croissance). Un heap qui grossit de 1 Mo par heure, c'est une fuite lente qui crashera le serveur dans quelques jours.

Recapitulatif des outils

Outil Usage Quand
--inspect + DevTools Heap snapshots interactifs Dev/staging
v8.writeHeapSnapshot() Snapshots programmatiques Dev/staging/prod
process.memoryUsage() Monitoring basique Partout
clinic doctor Diagnostic général Dev
clinic heapprofiler Profiling allocations Dev
0x Flamegraphs CPU Dev
prom-client Metriques Prometheus Production

Résumé

  • node --inspect connecte Chrome DevTools a un process Node.js
  • v8.writeHeapSnapshot() généré des snapshots depuis le code
  • process.memoryUsage() donne rss, heapTotal, heapUsed, external, arrayBuffers
  • clinic.js offre doctor (diagnostic général) et heapprofiler (allocations)
  • --max-old-space-size augmente la limite du heap (sans résoudre les fuites)
  • En production, exporter les metriques mémoire vers un système de monitoring

Article précédent : 09 - DevTools Memory Article suivant : 11 - Détecter et corriger les fuites mémoire

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.