Mémoire et performance JS/TS - 09 - DevTools Memory : investiguer dans Chrome

Heap snapshots, allocation timeline, retainers tree : utiliser Chrome DevTools pour traquer les fuites mémoire.

  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

09 - DevTools Memory : investiguer dans Chrome

Ce que tu vas apprendre

  • Naviguer dans l'onglet Memory de Chrome DevTools
  • Prendre et comparer des heap snapshots
  • Lire l'allocation timeline et l'allocation sampling
  • Comprendre le retainers tree, shallow size et retained size
  • Trouver les noeuds DOM detaches
  • Suivre un workflow d'investigation de fuite mémoire

Prerequisites

Avoir lu l'article 08 sur FinalizationRegistry.


Tu suspectes une fuite mémoire dans ton application web. L'onglet Performance te montre que la mémoire monte sans redescendre. Maintenant il faut trouver le coupable. C'est la que l'onglet Memory entre en jeu, et c'est un des outils les plus puissants que Chrome met a ta disposition.

L'onglet Memory

Ouvre DevTools (F12), onglet Memory. Tu as trois options de profiling :

  • Heap snapshot : photo de la mémoire a un instant T. Qui est la, combien il pese, qui le retient.
  • Allocation instrumentation on timeline : trace les allocations au fil du temps. Plus lourd mais tres precis.
  • Allocation sampling : echantillonnage des allocations. Moins precis mais moins coûteux en perf.

Pour la plupart des investigations, les heap snapshots suffisent.

Prendre un heap snapshot

  1. Ouvre l'onglet Memory
  2. Selectionne "Heap snapshot"
  3. Clique sur "Take snapshot"

Le snapshot liste tous les objets en mémoire, groupes par constructeur. Tu vois des entrees comme Array, Object, string, HTMLDivElement, et aussi tes propres classes.

Chaque entree montre :

  • Constructor : le type d'objet
  • Distance : la distance depuis la racine GC (les objets à distance 1 sont directement références par le scope global)
  • Shallow Size : la taille de l'objet lui-meme en octets
  • Retained Size : la taille de l'objet plus tout ce qu'il retient (ce qui serait libéré si cet objet etait collecte)

La distinction shallow/retained est fondamentale. Un objet peut faire 32 octets en shallow size mais retenir 50 Mo de donnees via ses références.

Comparer deux snapshots

C'est la technique la plus utile. Le principe :

  1. Amene ton application dans un état stable
  2. Prends un snapshot (Snapshot 1)
  3. Effectue l'action suspecte (ouvrir/fermer un modal, naviguer, etc.)
  4. Reviens a l'état initial
  5. Force un GC (icone poubelle dans l'onglet Performance)
  6. Prends un deuxieme snapshot (Snapshot 2)

Dans Snapshot 2, change la vue de "Summary" a "Comparison" et sélectionné Snapshot 1 comme référencé. Tu vois maintenant :

  • # New : objets créés entre les deux snapshots
  • # Deleted : objets supprimes
  • # Delta : la différence (positif = fuite potentielle)

Si tu as ouvert et ferme un modal et que Delta montre +500 HTMLDivElement, ces noeuds DOM ne sont pas liberes. C'est ta fuite.

Le retainers tree

Quand tu trouves un objet suspect, clique dessus. En bas, le panneau Retainers te montre la chaîne de références qui le maintient en vie.

HTMLDivElement @123456
  -> children in HTMLDivElement @789012
    -> element in Map @345678
      -> cache in AppComponent @901234
        -> context in Object @567890

Lis de bas en haut : l'objet global référencé un contexte, qui référencé un composant, qui a un cache (Map), qui contient l'élément DOM. Voila pourquoi il n'est pas collecte.

C'est souvent la que tu trouves le coupable : un event listener oublie, un cache qui grossit sans limite, un closure qui capture trop de variables.

Filtrer par constructeur

Dans la vue Summary, la barre de recherche en haut filtre par nom de constructeur. Tape le nom de ta classe :

UserSessionManager

Tu vois immédiatement combien d'instances existent. Si tu en attends 1 et que tu en vois 47, tu as un problème.

Pour les classes TypeScript compilees, le nom du constructeur correspond au nom de la classe. Avec les fonctions anonymes ou les objets litteraux, c'est plus difficile : ils apparaissent comme Object.

Trouver les noeuds DOM detaches

Tape "Detached" dans le filtre. Tu vois tous les Detached HTMLDivElement, Detached HTMLInputElement, etc. Ce sont des noeuds DOM qui ne sont plus dans l'arbre du document mais qui sont encore références en mémoire.

typescript// Exemple classique de noeud detache
class Tooltip {
  private element: HTMLDivElement;

  constructor() {
    this.element = document.createElement("div");
    document.body.appendChild(this.element);
  }

  hide(): void {
    this.element.remove(); // Retire du DOM
    // Mais this.element garde la reference !
  }

  // Il manque un destroy() qui ferait :
  // this.element = null;
}

Chaque fois que hide() est appele, le noeud est retire du DOM mais reste en mémoire via this.element. Si tu créés des centaines de tooltips, ca s'accumule.

Allocation timeline

L'allocation timeline enregistre les allocations en continu. Chaque barre bleue sur la timeline represente une allocation. Les barres qui restent bleues sont des objets toujours en vie. Les barres grises ont ete collectees.

Le workflow :

  1. Selectionne "Allocation instrumentation on timeline"
  2. Clique "Start"
  3. Effectue les actions suspectes dans l'app
  4. Clique "Stop"
  5. Selectionne une plage temporelle sur la timeline
  6. Analyse les objets alloues pendant cette plage

C'est plus precis que les snapshots pour les fuites qui s'accumulent lentement. Tu vois exactement quel moment de l'interaction alloue de la mémoire qui n'est jamais libérée.

Allocation sampling

Moins precis que la timeline mais beaucoup moins coûteux. Utile en production ou pour des sessions longues. Il echantillonne les allocations et te donne une vue statistique de qui alloue le plus.

La vue "Chart" montre un flamegraph des allocations. Les fonctions en haut du graphe sont celles qui allouent le plus de mémoire.

Workflow complet d'investigation

Voici ma méthode quand je suspecte une fuite sur paltemps.fr :

  1. Observer : onglet Performance, surveiller la courbe JS Heap. Si elle monte sans redescendre apres un GC force, il y a une fuite.

  2. Isoler : identifier l'action qui provoque la fuite. Ouvrir/fermer un composant ? Changer de page ? Recevoir des donnees WebSocket ?

  3. Snapshot avant : état stable, GC force, snapshot.

  4. Reproduire : effectuer l'action suspecte 5-10 fois (pas juste une fois, pour amplifier la fuite).

  5. Snapshot apres : GC force, deuxieme snapshot.

  6. Comparer : vue Comparison, trier par Delta decroissant.

  7. Investiguer : cliquer sur les objets avec un Delta positif eleve, regarder les retainers.

  8. Corriger : supprimer la référencé, ajouter un cleanup, utiliser WeakRef si pertinent.

  9. Verifier : refaire les étapes 3-6 pour confirmer que la fuite est colmatee.

Ce workflow est methodique et reproductible. Pas de devinette, pas de "je pense que c'est ca". Des preuves.

Les pièges courants

Les closures sont les fuites les plus sournoises. Un callback qui capture une variable locale empeche tout le scope d'etre collecte :

typescriptfunction setupHandler(bigData: ArrayBuffer) {
  const button = document.getElementById("btn");
  // Cette closure capture bigData, meme si elle ne l'utilise pas directement
  button?.addEventListener("click", () => {
    console.log("clicked");
    // bigData est dans le scope, il ne sera pas collecte
  });
}

Dans les retainers, tu verras bigData retenu par le scope de la closure. La solution : extraire le handler et ne capturer que ce qui est nécessaire.


Résumé

  • L'onglet Memory de Chrome DevTools offre trois modes : heap snapshot, allocation timeline, allocation sampling
  • Comparer deux snapshots est la technique la plus efficace pour trouver des fuites
  • Le retainers tree montre pourquoi un objet est encore en vie
  • Shallow size = l'objet seul, retained size = l'objet + tout ce qu'il retient
  • Filtrer par "Detached" pour trouver les noeuds DOM orphelins
  • Suivre un workflow methodique : observer, isoler, snapshot, comparer, corriger, vérifier

Article précédent : 08 - FinalizationRegistry Article suivant : 10 - Profiling mémoire en Node.js

Sources

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