Mémoire et performance JS/TS - 07 - WeakRef, WeakMap et WeakSet

Les références faibles en JavaScript : WeakRef, WeakMap, WeakSet, et comment construire un cache qui ne fuit pas.

  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

07 - WeakRef, WeakMap et WeakSet

Ce que tu vas apprendre

  • La différence entre références fortes et références faibles
  • Comment utiliser WeakRef et sa méthode deref()
  • WeakMap : clés faiblement referencees, cas d'usage (metadata, caches, DOM)
  • WeakSet : suivre des objets sans empecher leur libération
  • Construire un cache memory-safe avec WeakMap
  • Pourquoi les clés de WeakMap doivent etre des objets

Prerequisites


Le dilemme du cache

Tu veux un cache. Tu créés un Map. Tu stockes les résultats de calculs coûteux. Ca marche. Mais le Map garde des références fortes vers les valeurs. Tes objets ne seront jamais liberes par le GC. Ton cache devient une fuite mémoire.

J'ai vecu ca sur un projet ou on cachait des composants React dans un Map global pour éviter des re-renders. Au bout de 2 heures d'utilisation intensive, l'onglet pesait 1.5 Go. Le "cache" etait devenu un cimetiere d'objets.

C'est exactement le problème que les références faibles resolvent.

Références fortes vs faibles

Une référencé forte (c'est le défaut en JS) empeche le GC de libérer l'objet. Tant qu'il existe au moins une référencé forte, l'objet vit.

Une référencé faible ne compte pas pour le GC. Si la seule référencé restante vers un objet est une référencé faible, le GC peut libérer l'objet.

Reference forte :

  [variable] ---FORTE---> [objet]     GC: "je ne touche pas"

Reference faible :

  [WeakRef]  ---faible--> [objet]     GC: "aucune ref forte, je libere"
                              X
  [WeakRef]  ---faible--> (vide)      deref() retourne undefined

WeakRef : la référencé fragile

WeakRef est disponible depuis ES2021. Il te donne une référencé vers un objet sans empecher sa libération.

typescriptlet bigObject: { data: number[] } | null = {
  data: new Array(1_000_000).fill(42),
};

const weakRef = new WeakRef(bigObject);

// Tant que bigObject existe, deref() retourne l'objet
console.log(weakRef.deref()?.data.length); // 1000000

// On supprime la reference forte
bigObject = null;

// Apres un cycle GC (non deterministe), deref() retourne undefined
// Tu ne sais PAS quand exactement
console.log(weakRef.deref()); // undefined (apres GC) ou l'objet (avant GC)

Le point essentiel : deref() peut retourner undefined a tout moment apres que la dernière référencé forte a disparu. Ton code doit gerer ce cas.

typescriptfunction getFromWeakCache(ref: WeakRef<ExpensiveResult>): ExpensiveResult {
  const cached = ref.deref();
  if (cached !== undefined) {
    return cached; // encore en memoire, on reutilise
  }
  // L'objet a ete collecte, on recalcule
  return computeExpensiveResult();
}

WeakMap : le vrai outil du quotidien

WeakRef est utile mais bas niveau. En pratique, tu utiliseras surtout WeakMap. C'est un Map ou les clés sont faiblement referencees.

typescriptconst metadata = new WeakMap<object, string>();

let user = { name: "Alice" };
metadata.set(user, "admin");

console.log(metadata.get(user)); // "admin"

user = null; // plus de reference forte vers l'objet
// Le GC peut liberer l'objet ET l'entree dans le WeakMap
// Pas de fuite memoire

Avec un Map classique :

typescriptconst metadata = new Map<object, string>();
let user = { name: "Alice" };
metadata.set(user, "admin");
user = null;
// L'objet est TOUJOURS dans le Map (reference forte via la cle)
// Fuite memoire

Pourquoi les clés doivent etre des objets

Les clés d'un WeakMap doivent etre des objets (ou des symboles non enregistres depuis ES2023). Pas de strings, pas de numbers.

La raison : les primitives n'ont pas d'identité. Le number 42 est le meme partout. Il n'y a pas de "référencé" vers un number qu'on pourrait affaiblir. Le GC ne "libéré" pas le concept de 42.

typescriptconst wm = new WeakMap();

// OK
wm.set({}, "value");
wm.set(document.body, "metadata");
wm.set(Symbol("unique"), "data"); // ES2023+

// ERREUR
// wm.set("string", "value");     // TypeError
// wm.set(42, "value");           // TypeError
// wm.set(null, "value");         // TypeError

Cas d'usage 1 : metadata sur des objets

Tu veux attacher des informations a un objet sans modifier l'objet lui-meme. Et tu veux que ces informations soient liberees quand l'objet est libéré.

typescriptconst timestamps = new WeakMap<object, number>();

function track(obj: object) {
  timestamps.set(obj, Date.now());
}

function getAge(obj: object): number | undefined {
  const created = timestamps.get(obj);
  return created ? Date.now() - created : undefined;
}

let session = { id: "abc123" };
track(session);
// ...
session = null; // le timestamp est libere avec la session

Cas d'usage 2 : cache memory-safe

typescriptclass ComputeCache {
  private cache = new WeakMap<object, Map<string, unknown>>();

  get<T>(target: object, key: string, compute: () => T): T {
    let targetCache = this.cache.get(target);

    if (!targetCache) {
      targetCache = new Map();
      this.cache.set(target, targetCache);
    }

    if (targetCache.has(key)) {
      return targetCache.get(key) as T;
    }

    const result = compute();
    targetCache.set(key, result);
    return result;
  }
}

const cache = new ComputeCache();
let data = { values: [1, 2, 3, 4, 5] };

const sum = cache.get(data, "sum", () =>
  data.values.reduce((a, b) => a + b, 0)
);

// Quand data est libere, le cache associe l'est aussi
data = null;

Cas d'usage 3 : associations DOM

typescript// Associer des donnees a des elements DOM
const elementData = new WeakMap<HTMLElement, ComponentState>();

function initComponent(el: HTMLElement) {
  const state: ComponentState = {
    isOpen: false,
    items: [],
  };
  elementData.set(el, state);
}

function getState(el: HTMLElement): ComponentState | undefined {
  return elementData.get(el);
}

// Quand l'element est retire du DOM et qu'aucune reference JS ne subsiste,
// le state associe est libere automatiquement

C'est d'ailleurs comme ca que beaucoup de frameworks gerent l'état interne des composants.

WeakSet : suivre sans retenir

WeakSet est comme un Set mais les éléments sont faiblement références. Tu ne peux pas itérer sur un WeakSet (c'est par design : les éléments peuvent disparaître a tout moment).

typescriptconst processed = new WeakSet<object>();

function processOnce(obj: object) {
  if (processed.has(obj)) {
    return; // deja traite
  }
  processed.add(obj);
  doExpensiveWork(obj);
}

let item = { id: 1 };
processOnce(item); // traite
processOnce(item); // ignore

item = null; // l'entree dans le WeakSet est liberee

Cas d'usage typique : marquer des objets comme "vus", "valides", "initialises" sans empecher leur libération.

Les limites des références faibles

Les WeakMap et WeakSet ne sont pas itérables. Tu ne peux pas faire weakMap.keys(), weakMap.values(), weakMap.entries(), ou weakMap.size. C'est voulu : puisque les entrees peuvent disparaître a tout moment, une itération donnerait des résultats non deterministes.

typescriptconst wm = new WeakMap();
// wm.keys()     // n'existe pas
// wm.values()   // n'existe pas
// wm.forEach()  // n'existe pas
// wm.size       // n'existe pas

Si tu as besoin d'itérer sur tes entrees, tu as besoin d'un Map classique (avec un mecanisme de nettoyage manuel, comme un TTL).

Un vrai cas sur paltemps.fr

Sur paltemps.fr, je cache les résultats de geocoding (transformer un nom de ville en coordonnees GPS). Avant, j'utilisais un Map<string, Coordinates> avec le nom de ville comme clé. Le problème : les clés sont des strings, donc pas compatible avec WeakMap.

La solution : utiliser l'objet "requête de recherche" comme clé dans un WeakMap. Quand l'utilisateur navigue vers une autre page et que l'objet requête sort du scope, le cache associe est libéré. Pour le cache persistant (entre les navigations), j'utilise un Map classique avec un TTL de 5 minutes et une limite de 200 entrees.

typescript// Cache court terme (libere avec la page)
const pageCache = new WeakMap<SearchRequest, GeoResult>();

// Cache long terme (avec limite)
const globalCache = new Map<string, { result: GeoResult; expires: number }>();

function geocode(request: SearchRequest): GeoResult {
  // Verifier le cache court terme
  const cached = pageCache.get(request);
  if (cached) return cached;

  // Verifier le cache long terme
  const key = request.city;
  const global = globalCache.get(key);
  if (global && global.expires > Date.now()) {
    pageCache.set(request, global.result);
    return global.result;
  }

  // Calculer et mettre en cache
  const result = fetchGeocode(request.city);
  pageCache.set(request, result);
  globalCache.set(key, { result, expires: Date.now() + 300_000 });

  // Limiter la taille du cache global
  if (globalCache.size > 200) {
    const firstKey = globalCache.keys().next().value;
    globalCache.delete(firstKey);
  }

  return result;
}

Résumé

  • Les références fortes (par défaut) empechent le GC de libérer un objet. Les références faibles ne comptent pas.
  • WeakRef donne une référencé faible vers un objet. deref() retourne l'objet ou undefined.
  • WeakMap est un Map a clés faibles : quand l'objet-clé est libéré, l'entree disparaît. Pas de fuite.
  • WeakSet permet de marquer des objets sans les retenir.
  • Les clés de WeakMap doivent etre des objets (pas de primitives).
  • WeakMap et WeakSet ne sont pas itérables (par design).
  • Utilise WeakMap pour les metadata, les caches, et les associations DOM.

Precedent : Closures et mémoire Suivant : FinalizationRegistry

Sources

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