Stop Leaking Memory with WeakMap
Fixing weak code with weak references!
You know that feeling when you change one line of code and watch your memory usage drop by 50%? I had that moment watching Chrome’s DevTools Performance Monitor as a dashboard app went from hemorrhaging 100MB an hour to running clean for an entire afternoon.
The one-line change: new Map() became new WeakMap().
That’s it. Same API surface, same usage pattern, completely different behavior under the hood. But understanding why this works means understanding something most JavaScript developers never think about: what happens when nothing is looking at your data anymore.
When References Become Anchors
A regular Map in JavaScript treats its keys like precious cargo. Once you put something in there, the Map will hold onto it with an iron grip. The Garbage Collector sees this relationship and thinks, “Clearly they still need this object, better not touch it.”
This protective instinct becomes a problem when you’re storing metadata about temporary things. DOM nodes that get removed. User sessions that expire. Component instances that unmount. The Map doesn’t know these objects are done being useful. It just knows it has a reference, so it keeps them alive.
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// The element is gone from the DOM, but cache is keeping it in memoryThe Garbage Collector can’t clean up element because cache is still pointing at it. This is called a “strong reference,” and in long-running Single Page Apps, it becomes a leak that eventually crashes the browser.
WeakMap Changes the Rules
A WeakMap works differently. It treats its keys as temporary citizens rather than permanent residents. When you store something in a WeakMap, you’re essentially saying: “I want to associate this data with this object, but I don’t want to be the reason it stays alive.”
If the only thing keeping an object in memory is a WeakMap, the Garbage Collector is allowed to take it. When the object disappears, the WeakMap entry disappears with it. No manual cleanup needed.
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// The element gets Garbage Collected// The cache entry vanishes automaticallyI ran a benchmark creating 100,000 DOM nodes, storing metadata about each one, then removing them all. With a Map, the browser held onto 150-200MB. With a WeakMap, it dropped to 70-80MB. Same code, same functionality, half the memory footprint.
What You Give Up
WeakMap has constraints that feel like limitations until you realize they’re what make the magic work.
You can’t iterate over a WeakMap. No forEach, no keys(), no values(). This makes sense when you think about it: the Garbage Collector might delete an entry in the middle of your loop. Do you really want to deal with that?
You can’t check the size. No .size property, no .length. Again, it’s a moving target. The number could change between when you ask and when you get the answer.
Keys must be objects. No strings, no numbers, no primitives. This is fundamental to how weak references work: primitive values don’t have identity that can be tracked separately from their value.
These aren’t bugs. They’re the design. WeakMap is built for one specific job: attaching metadata to objects without preventing those objects from being cleaned up. If you need iteration or primitive keys or a count of entries, you’re probably solving a different problem and should use a regular Map.
Where This Actually Helps
The “private data” pattern was WeakMap’s original use case, back before JavaScript had #private fields. Libraries would create a WeakMap outside the class and use it to store data that shouldn’t be accessible on the instance.
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}When a User instance gets Garbage Collected, the private data goes with it. No cleanup code needed.
Memoization is another natural fit, especially when you’re caching results based on object inputs rather than primitive values. If your expensive calculation takes a config object as input, a WeakMap means you don’t have to worry about the cache outliving the configs.
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;}The cache only lives as long as the objects being cached. Once obj isn’t referenced anywhere else, both the cached result and the cache entry disappear together.
When to Reach for It
Memory leaks in modern web apps usually come from stale references to things that should have been cleaned up. If you’re building anything long-running, a dashboard that stays open all day, a chat app that runs for hours, an admin panel that never refreshes, you need to think about what happens to old data.
WeakMap is particularly useful when you’re associating data with DOM nodes, component instances, or any object whose lifetime you don’t control. If you’re storing something based on a reference and that reference might go away, WeakMap makes the cleanup much simpler.
Regular Map is still the right choice when you’re building an actual cache with eviction policies, when you need to iterate over entries, when you’re using primitive keys, or when the data itself is what matters rather than its association with an object.
The nice thing about WeakMap is that it’s usually obvious when you need it. If you find yourself writing cleanup code to remove map entries when objects are destroyed, that’s a sign. If you’re worried about memory growing unbounded because you’re not sure when to delete things, that’s another sign.
Sometimes the best feature is one that just works without you having to think about it.
Resources
- MDN: WeakMap
- MDN: Memory Management
- V8 Blog: Weak References and Finalizers
- JavaScript.info: WeakMap and WeakSet



