JavaScript has a garbage collector, so most developers never think about memory management. Then they build a client-side cache, or a component that attaches data to DOM nodes, or a pub/sub system — and slowly watch memory climb with no obvious cause. WeakMap and WeakRef exist specifically for these situations. They’re the most powerful and least understood primitives in the language.
⚡ TL;DR:
WeakMapassociates data with objects without preventing garbage collection.WeakRefholds a reference to an object that the GC is still allowed to collect. Use WeakMap for object metadata, WeakRef for caches. Both are invisible to the GC’s reachability analysis.
The Problem: Regular Map Creates Memory Leaks
// ❌ Classic memory leak: caching data tied to DOM nodes
const cache = new Map();
function processElement(element) {
if (!cache.has(element)) {
cache.set(element, computeExpensiveData(element));
}
return cache.get(element);
}
// When element is removed from DOM:
document.body.removeChild(element);
// element is removed from DOM, but cache still holds a reference!
// GC cannot collect element — MEMORY LEAK
// This is why SPAs slowly consume gigabytes over a session
// ✅ Fix: WeakMap — element can be GC'd even while in cache
const cache = new WeakMap();
function processElement(element) {
if (!cache.has(element)) {
cache.set(element, computeExpensiveData(element));
}
return cache.get(element);
}
// When element is removed from DOM and has no other references,
// GC collects it AND automatically removes the WeakMap entry
// Zero memory leak. Zero cleanup code needed.
WeakMap Deep Dive — What It Can and Can’t Do
JavaScript memory and performance
→ Complete JavaScript Course (Udemy) — Covers memory management, WeakMap, WeakRef, and garbage collection.
Sponsored links. We may earn a commission at no extra cost to you.
const wm = new WeakMap();
// Keys MUST be objects (not primitives)
wm.set({}, 'value'); // ✅ object key
wm.set('string', 'x'); // ❌ TypeError: Invalid value used as weak map key
// WeakMap is NOT iterable (by design)
// You cannot enumerate keys/values — the GC controls key existence
for (const [k, v] of wm) {} // ❌ TypeError: WeakMap is not iterable
wm.size; // undefined — size is unknowable (GC may have collected keys)
// API: only has/get/set/delete
const key = { id: 1 };
wm.set(key, { computed: 'data', timestamp: Date.now() });
wm.has(key); // true
wm.get(key); // { computed: 'data', timestamp: ... }
wm.delete(key); // true
// Real use case: private class data (pre-#private syntax)
const _private = new WeakMap();
class BankAccount {
constructor(balance) {
_private.set(this, { balance, transactions: [] });
}
deposit(amount) {
const data = _private.get(this);
data.balance += amount;
data.transactions.push({ type: 'deposit', amount });
}
get balance() { return _private.get(this).balance; }
}
// When account instance is GC'd, _private entry disappears automatically
WeakRef — A Nullable Pointer for JavaScript
// WeakRef holds a reference that doesn't prevent GC
// deref() returns the object OR undefined if it was collected
let expensiveObject = { data: new ArrayBuffer(1024 * 1024) }; // 1MB
const ref = new WeakRef(expensiveObject);
// Later, expensiveObject might be collected...
expensiveObject = null; // Remove the strong reference
// Try to use the object:
const obj = ref.deref();
if (obj) {
console.log('Object still alive:', obj.data.byteLength);
} else {
console.log('Object was garbage collected');
}
// You MUST check deref() result — it can be undefined at any time
// IMPORTANT: The GC can collect the object between any two deref() calls
// Never store deref() result across async operations:
const obj1 = ref.deref(); // alive
await someAsyncOperation();
const obj2 = ref.deref(); // might be undefined — obj1 and obj2 can differ!
// ✅ Correct: call deref() once, check result, use it synchronously
FinalizationRegistry — Run Code When Objects Are Collected
// FinalizationRegistry: callback when a WeakRef target is GC'd
const registry = new FinalizationRegistry((heldValue) => {
console.log('Object collected, held value:', heldValue);
// Clean up external resources (file handles, network connections, etc.)
});
let resource = { connection: createDbConnection() };
registry.register(resource, 'db-connection-cleanup');
// When resource is GC'd, callback runs with 'db-connection-cleanup'
// Practical use: LRU cache with automatic cleanup
class WeakCache {
#cache = new Map();
#registry = new FinalizationRegistry(key => this.#cache.delete(key));
set(key, value) {
const ref = new WeakRef(value);
this.#cache.set(key, ref);
this.#registry.register(value, key);
}
get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;
const value = ref.deref();
if (!value) {
this.#cache.delete(key); // Clean up stale entry proactively
return undefined;
}
return value;
}
}
const cache = new WeakCache();
let bigData = { results: new Array(100000).fill(0) };
cache.set('query-results', bigData);
bigData = null; // When collected, cache entry auto-removes
The Observer Pattern Without Leaks
// ❌ Classic event listener leak
class EventEmitter {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(handler);
}
// If subscriber is destroyed without calling off(), handler keeps it alive
}
// ✅ WeakRef-based observer — subscribers can be GC'd
class WeakEventEmitter {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(new WeakRef(handler));
}
emit(event, data) {
const handlers = this.#listeners.get(event);
if (!handlers) return;
for (const ref of handlers) {
const handler = ref.deref();
if (handler) {
handler(data);
} else {
handlers.delete(ref); // Auto-cleanup dead refs
}
}
}
}
// Now subscribers don't need to explicitly unsubscribe
// When component is destroyed and handler function GC'd,
// next emit() call automatically cleans up the dead WeakRef
WeakMap vs WeakRef vs WeakSet Cheat Sheet
- ✅ WeakMap — associate metadata with objects, object as key, auto-cleanup on GC
- ✅ WeakRef — cache expensive objects, always check
deref()result - ✅ WeakSet — track a set of objects without preventing their collection
- ✅ FinalizationRegistry — run cleanup callbacks when objects are collected
- ❌ Never store
deref()result acrossawait— object may be collected between calls - ❌ Never use WeakMap/WeakRef for data that must persist — use regular Map/strong references
- ❌ Never rely on FinalizationRegistry for critical cleanup — GC timing is non-deterministic
These memory primitives become especially important when combined with the V8 JIT optimizer — WeakMap lookups are monomorphic and get JIT-compiled to near-native speed. For server-side memory management in Node.js, pair these techniques with the stream backpressure guide for complete memory control. External reference: MDN JavaScript Memory Management.
Recommended resources
- You Don’t Know JS: Types & Grammar — The chapter on object references and value semantics is the foundation for understanding why WeakMap’s non-preventing reference model matters.
- High Performance JavaScript — Covers memory profiling, garbage collection tuning, and the exact scenarios where WeakMap and WeakRef prevent the leaks described in this post.
Disclosure: This post contains affiliate links. If you purchase through these links, CheatCoders earns a small commission at no extra cost to you. We only recommend tools and books we genuinely find valuable.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: JavaScript Generator Functions: Lazy Evaluation and Infinite Sequences Done Right - CheatCoders