Arrêtez de fuir la mémoire avec WeakMap
Réparer du code fragile avec des références faibles !
Vous connaissez cette sensation quand vous changez une seule ligne de code et que vous regardez votre consommation mémoire chuter de 50 % ? J’ai vécu ce moment devant le Moniteur de performances des DevTools de Chrome, pendant qu’une application de dashboard passait d’une hémorragie de 100 Mo par heure à un fonctionnement impeccable pendant tout un après-midi.
Le changement d’une seule ligne : new Map() est devenu new WeakMap().
C’est tout. Même surface d’API, même schéma d’utilisation, comportement totalement différent sous le capot. Mais comprendre pourquoi cela fonctionne suppose de comprendre quelque chose que la plupart des développeurs JavaScript ne pensent jamais : ce qui arrive quand plus personne ne regarde vos données.
Quand les références deviennent des ancres
Un Map classique en JavaScript traite ses clés comme un chargement précieux. Une fois que vous y avez placé quelque chose, le Map s’y accroche fermement. Le ramasse-miettes voit cette relation et se dit : « Visiblement, ils ont encore besoin de cet objet, mieux vaut ne pas y toucher. »
Cet instinct protecteur devient un problème quand vous stockez des métadonnées sur des choses temporaires. Des nœuds du DOM qui se font retirer. Des sessions utilisateur qui expirent. Des instances de composants qui se démontent. Le Map ne sait pas que ces objets ont fini d’être utiles. Il sait juste qu’il a une référence, donc il les maintient en vie.
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// L'élément a disparu du DOM, mais le cache le maintient en mémoireLe ramasse-miettes ne peut pas nettoyer element parce que cache pointe toujours vers lui. C’est ce qu’on appelle une « référence forte », et dans les applications monopages qui tournent longtemps, cela devient une fuite qui finit par faire planter le navigateur.
WeakMap change les règles
Un WeakMap fonctionne différemment. Il traite ses clés comme des citoyens temporaires plutôt que comme des résidents permanents. Quand vous stockez quelque chose dans un WeakMap, vous dites en substance : « Je veux associer ces données à cet objet, mais je ne veux pas être la raison pour laquelle il reste en vie. »
Si la seule chose qui maintient un objet en mémoire est un WeakMap, le ramasse-miettes est autorisé à le récupérer. Quand l’objet disparaît, l’entrée du WeakMap disparaît avec lui. Pas besoin de nettoyage manuel.
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// L'élément est récupéré par le ramasse-miettes// L'entrée du cache disparaît automatiquementJ’ai fait un benchmark en créant 100 000 nœuds du DOM, en stockant des métadonnées sur chacun d’eux, puis en les supprimant tous. Avec un Map, le navigateur conservait 150 à 200 Mo. Avec un WeakMap, le chiffre tombait à 70-80 Mo. Même code, même fonctionnalité, empreinte mémoire divisée par deux.
Ce à quoi vous renoncez
WeakMap a des contraintes qui ressemblent à des limitations jusqu’à ce que vous compreniez que c’est précisément ce qui permet la magie.
Vous ne pouvez pas parcourir un WeakMap. Pas de forEach, pas de keys(), pas de values(). Cela a du sens quand on y réfléchit : le ramasse-miettes pourrait supprimer une entrée en plein milieu de votre boucle. Vraiment envie de gérer ça ?
Vous ne pouvez pas connaître la taille. Pas de propriété .size, pas de .length. Encore une fois, c’est une cible mouvante. Le nombre pourrait changer entre le moment où vous demandez et celui où vous obtenez la réponse.
Les clés doivent être des objets. Pas de chaînes, pas de nombres, pas de primitives. C’est fondamental pour le fonctionnement des références faibles : les valeurs primitives n’ont pas d’identité qui puisse être suivie indépendamment de leur valeur.
Ce ne sont pas des bugs. C’est la conception. WeakMap est conçu pour un travail précis : attacher des métadonnées à des objets sans empêcher leur nettoyage. Si vous avez besoin d’itération, de clés primitives ou d’un décompte des entrées, vous résolvez probablement un problème différent et devriez utiliser un Map classique.
Où cela aide concrètement
Le motif « données privées » était le cas d’usage originel de WeakMap, avant que JavaScript n’ait les champs #private. Les bibliothèques créaient un WeakMap en dehors de la classe et l’utilisaient pour stocker des données qui ne devaient pas être accessibles sur l’instance.
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}Quand une instance de User est récupérée par le ramasse-miettes, les données privées disparaissent avec elle. Pas besoin de code de nettoyage.
La mémoïsation est un autre cas d’usage naturel, surtout quand vous mettez en cache des résultats basés sur des objets en entrée plutôt que sur des valeurs primitives. Si votre calcul coûteux prend un objet de configuration en entrée, un WeakMap vous évite de vous soucier du fait que le cache survive aux configurations.
const cache = new WeakMap();
function expensiveCalc(obj) { if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj); cache.set(obj, result); return result;}Le cache ne vit que tant que les objets mis en cache existent. Une fois que obj n’est plus référencé nulle part ailleurs, le résultat mis en cache et l’entrée du cache disparaissent ensemble.
Quand l’utiliser
Les fuites mémoire dans les applications web modernes viennent généralement de références obsolètes vers des choses qui auraient dû être nettoyées. Si vous construisez quelque chose de longue durée — un dashboard ouvert toute la journée, une appli de chat qui tourne pendant des heures, un panneau d’admin qui ne se rafraîchit jamais —, vous devez penser à ce qui arrive aux anciennes données.
WeakMap est particulièrement utile quand vous associez des données à des nœuds du DOM, des instances de composants, ou tout objet dont vous ne contrôlez pas la durée de vie. Si vous stockez quelque chose basé sur une référence et que cette référence peut disparaître, WeakMap simplifie considérablement le nettoyage.
Un Map classique reste le bon choix quand vous construisez un vrai cache avec des politiques d’éviction, quand vous devez parcourir les entrées, quand vous utilisez des clés primitives, ou quand les données elles-mêmes importent plus que leur association avec un objet.
Le côté appréciable de WeakMap, c’est qu’il est généralement évident quand vous en avez besoin. Si vous vous surprenez à écrire du code de nettoyage pour supprimer des entrées de map quand des objets sont détruits, c’est un signe. Si vous vous inquiétez que la mémoire croisse sans limites parce que vous n’êtes pas sûr de quand supprimer les choses, c’en est un autre.
Parfois, la meilleure fonctionnalité est celle qui fonctionne toute seule, sans qu’on ait à y penser.
Ressources
- MDN : WeakMap
- MDN : Gestion de la mémoire
- Blog V8 : Références faibles et finaliseurs
- JavaScript.info : WeakMap et WeakSet