Mémoire et performance JS/TS - 01 - Stack vs Heap

Comprendre ou vivent tes variables : la stack pour les primitives, le heap pour les objets, et pourquoi ca change tout.

  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

01 - Stack vs Heap : ou vivent tes variables

Ce que tu vas apprendre

  • Ce qu'est la stack (pile d'appels) et comment elle fonctionne
  • Ce qu'est le heap (tas) et pourquoi les objets y vivent
  • Pourquoi les primitives sont sur la stack et les objets sur le heap
  • Ce qui se passe quand la stack deborde

Prerequisites


La metaphore du bureau

Imagine ton bureau. Tu as une pile de post-its a droite (la stack) et un grand tiroir a gauche (le heap). Quand tu appelles une fonction, tu poses un post-it sur la pile avec les infos locales : les arguments, les variables simples. Quand la fonction se termine, tu retires le post-it. Simple, rapide, automatique.

Mais quand tu créés un objet, un tableau, une fonction, ca ne tient pas sur un post-it. Tu le ranges dans le tiroir (le heap), et tu notes sur ton post-it l'adresse du tiroir ou le trouver. C'est ca, une référencé.

La stack : rapide, ordonnee, limitee

La stack est une structure LIFO (Last In, First Out). Chaque appel de fonction créé un call frame (cadre d'appel) qui contient :

  • Les arguments de la fonction
  • Les variables locales (primitives : number, string, boolean, "null", "undefined", bigint, symbol)
  • L'adresse de retour (ou reprendre apres la fonction)
typescriptfunction add(a: number, b: number): number {
  const result = a + b;
  return result;
}

function main() {
  const x = 10;
  const y = 20;
  const sum = add(x, y);
  console.log(sum);
}

main();

Voici ce qui se passe dans la stack pendant l'exécution :

Etape 1 : main() est appelee
+-----------------+
| main            |
| x = 10          |
| y = 20          |
+-----------------+

Etape 2 : add(10, 20) est appelee
+-----------------+
| add             |
| a = 10          |
| b = 20          |
| result = 30     |
+-----------------+
| main            |
| x = 10          |
| y = 20          |
+-----------------+

Etape 3 : add() retourne 30, son frame est retire
+-----------------+
| main            |
| x = 10          |
| y = 20          |
| sum = 30        |
+-----------------+

La stack est rapide parce que l'allocation et la desallocation sont triviales : on deplace un pointeur. Pas besoin de chercher un espace libre. Pas besoin de garbage collector.

Mais la stack est limitee en taille. Sur V8 (Node.js/Chrome), c'est typiquement quelques Mo. Suffisant pour des milliers d'appels imbriques, mais pas pour des millions.

Le heap : flexible, coûteux, gere par le GC

Le heap est une zone de mémoire beaucoup plus grande (des centaines de Mo a plusieurs Go). C'est la que vivent tous les objets, tableaux, fonctions, closures, et tout ce qui a une taille dynamique.

typescriptfunction createUser(name: string, age: number) {
  // Cet objet est alloue sur le heap
  const user = {
    name,       // la string est aussi sur le heap
    age,        // le number est copie dans l'objet sur le heap
    scores: [], // le tableau vide est sur le heap
  };
  return user;
}

const alice = createUser("Alice", 30);

Quand tu ecris const alice = createUser(...), la variable alice est sur la stack. Mais sa valeur est une référencé (une adresse) qui pointe vers l'objet sur le heap.

        STACK                          HEAP
  +---------------+            +-------------------+
  | alice = 0xA3  | ---------> | { name: "Alice",  |
  +---------------+            |   age: 30,        |
                               |   scores: []      |
                               +-------------------+

Primitives vs objets : la regle

La regle est simple :

  • Primitives (number, string, boolean, "null", "undefined", bigint, symbol) : stockees directement sur la stack (dans le call frame)
  • Objets (objets, tableaux, fonctions, Map, Set, etc.) : alloues sur le heap, référencé sur la stack

Ca explique un comportement que tu connais deja :

typescript// Primitives : copie de valeur
let a = 42;
let b = a;
b = 100;
console.log(a); // 42 - pas affecte

// Objets : copie de reference
let obj1 = { value: 42 };
let obj2 = obj1;
obj2.value = 100;
console.log(obj1.value); // 100 - affecte !
Primitives (copie de valeur) :

  STACK
  +-------+      +-------+
  | a = 42|      | b = 100|   (deux valeurs independantes)
  +-------+      +-------+

Objets (copie de reference) :

  STACK                       HEAP
  +-------------+        +--------------+
  | obj1 = 0xB7 | ------>| { value: 100 }|
  +-------------+   /    +--------------+
  | obj2 = 0xB7 | --/
  +-------------+
  (deux references vers le meme objet)

Stack overflow : quand la pile deborde

La stack a une taille fixe. Si tu appelles trop de fonctions sans que les précédentes se terminent, tu depasses la limite :

typescriptfunction infinite(): void {
  infinite(); // appel recursif sans condition d'arret
}

infinite();
// RangeError: Maximum call stack size exceeded

Chaque appel a infinite() ajoute un frame sur la stack. Apres ~10 000 a ~15 000 appels (ca depend du moteur et de la taille des frames), la stack est pleine. C'est le fameux stack overflow.

C'est aussi pour ca que les algorithmes recursifs profonds posent problème en JavaScript. Pas de tail call optimization fiable (seul Safari l'implemente). Si tu as une recursion de 100 000 niveaux, il faut la transformer en boucle.

Le cas special des strings

Les strings en JavaScript sont des primitives... mais pas vraiment stockees sur la stack quand elles sont longues. V8 les alloue sur le heap et met la référencé sur la stack. Les petites strings (quelques caractères) peuvent etre internalisees (string interning) et partagees.

typescriptconst a = "hello"; // probablement internalisee
const b = "hello"; // meme reference interne que a
const c = "a".repeat(10_000); // forcement sur le heap

C'est un détail d'implementation de V8, mais ca explique pourquoi la concatenation massive de strings peut consommer beaucoup de mémoire. On en reparlera dans l'article sur les strings.

En pratique sur paltemps.fr

Sur paltemps.fr, les donnees meteo arrivent sous forme de gros tableaux JSON (previsions heure par heure sur 7 jours). Chaque tableau est un objet sur le heap. Si je gardais toutes les réponses API en mémoire pour faire du cache, le heap explosait en quelques heures. Comprendre que ces objets vivent sur le heap et que le GC ne les libéré que quand plus rien ne les référencé, c'est ce qui m'a amene a utiliser un cache avec TTL.

Résumé

  • La stack est rapide, ordonnee (LIFO), et contient les primitives et les références. Elle a une taille fixe.
  • Le heap est grand, flexible, et contient tous les objets. Il est gere par le garbage collector.
  • Les primitives sont copiees par valeur. Les objets sont copies par référencé.
  • Un stack overflow arrive quand la pile d'appels dépassé sa taille limite (recursion infinie ou trop profonde).

Precedent : Introduction - pourquoi la mémoire compte Suivant : Cycle de vie de la mémoire

Sources

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