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
WeakRefet sa méthodederef() 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
WeakMapdoivent etre des objets
Prerequisites
- Avoir lu Closures et mémoire
- Comprendre ce que le GC libéré et pourquoi
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.
WeakRefdonne une référencé faible vers un objet.deref()retourne l'objet ouundefined.WeakMapest unMapa clés faibles : quand l'objet-clé est libéré, l'entree disparaît. Pas de fuite.WeakSetpermet de marquer des objets sans les retenir.- Les clés de
WeakMapdoivent etre des objets (pas de primitives). WeakMapetWeakSetne sont pas itérables (par design).- Utilise
WeakMappour les metadata, les caches, et les associations DOM.
Precedent : Closures et mémoire Suivant : FinalizationRegistry