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.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
You just read something most developers never learn. Get more secrets like this delivered every week — JavaScript internals, Python optimizations, AWS architectures, system design, and AI workflows.
Join 1,000+ senior developers who actually level up. Zero fluff, pure signal.
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