Memory Management & WeakRef
Understand how JavaScript's garbage collector works, prevent memory leaks, and use WeakRef and FinalizationRegistry for advanced cache and lifecycle patterns.
Overview
Key Points
- •JavaScript uses mark-and-sweep garbage collection: any reachable object survives; unreachable objects are collected
- •Memory leaks occur when references are accidentally retained the GC cannot collect reachable objects even if unused
- •Detached DOM nodes still in memory: remove event listeners before removing elements, or use event delegation
- •WeakMap and WeakSet hold keys weakly the entry is automatically removed when the key object is GC'd
- •WeakRef wraps an object without preventing collection; always check deref() !== undefined before using the result
- •FinalizationRegistry fires a callback after an object is collected use for optional cleanup, never critical logic
- •Closures capture their entire scope chain accidentally capturing large arrays or buffers causes persistent leaks
- •Chrome DevTools heap snapshots and allocation timelines are the primary tools for diagnosing real memory leaks
Examples
Common memory leak patterns and their fixes
// LEAK 1: Event listener not removed
class Component {
constructor(element) {
this.element = element;
this._handler = this.onClick.bind(this);
this.element.addEventListener('click', this._handler);
}
onClick() { console.log('clicked'); }
// Without destroy(), handler holds reference to component and element forever
destroy() {
this.element.removeEventListener('click', this._handler);
this.element = null;
}
}
// LEAK 2: Forgotten interval
function startPolling() {
const data = new Array(100000).fill('some large data');
const id = setInterval(() => {
// closure captures 'data' even if you call clearInterval elsewhere
// if you lose the reference to 'id', this leaks forever
console.log(data.length);
}, 1000);
return id; // MUST hold onto this and call clearInterval(id) when done
}
// FIX: WeakMap for metadata (entry auto-removed when element is GC'd)
const metadata = new WeakMap();
function tagElement(el, info) {
metadata.set(el, info);
}
const el = document.createElement('div');
tagElement(el, { role: 'widget', priority: 1 });
console.log(metadata.get(el)); // { role: 'widget', priority: 1 }
// When 'el' goes out of scope, the WeakMap entry is automatically removedWeakMap is perfect for associating data with DOM nodes or objects when you do not own them and cannot add properties to them.
WeakRef a weak cache that does not prevent garbage collection
// A cache that holds objects weakly GC can reclaim them when memory is tight
class WeakCache {
#cache = new Map();
set(key, value) {
this.#cache.set(key, new WeakRef(value));
}
get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;
const value = ref.deref();
if (value === undefined) {
// Object was GC'd prune the dead entry
this.#cache.delete(key);
return undefined;
}
return value;
}
has(key) {
return this.get(key) !== undefined;
}
}
const cache = new WeakCache();
let bigData = { payload: new Array(1_000_000).fill(0) };
cache.set('key1', bigData);
console.log(cache.get('key1') !== undefined); // true
// If 'bigData' goes out of scope and GC runs, cache.get('key1') returns undefined
// This allows the cache to hold objects loosely without causing OOMAlways check deref() result never assume the object is still alive between two calls, even in synchronous code.
FinalizationRegistry cleanup callbacks after GC
// Track when objects are collected (e.g. resource handles)
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with token "${heldValue}" was garbage collected`);
// heldValue is the "token" you registered NOT the object itself
// (you cannot hold a reference to the collected object here)
});
function createResource(name) {
const resource = { data: new Array(100000).fill(name) };
registry.register(resource, name); // second arg is the token
return resource;
}
let r1 = createResource('database-connection');
let r2 = createResource('file-handle');
r1 = null; // eligible for GC
r2 = null; // eligible for GC
// At some future point (unpredictable timing):
// "Object with token "database-connection" was garbage collected"
// "Object with token "file-handle" was garbage collected"
//
// GC timing is NOT guaranteed do not rely on this for correctnessFinalizationRegistry callbacks fire asynchronously and may never fire at all in short-lived programs. Use them only for optional diagnostics or logging.
Common Mistakes
- Using FinalizationRegistry for resource cleanup that must happen deterministically GC timing is not guaranteed; use explicit dispose methods or the Disposable protocol instead
- Assuming WeakRef.deref() is stable within a synchronous block technically GC can happen between microtasks, so always re-deref if you yield control
- Storing closures in long-lived data structures (globals, caches) that capture large local scope, unintentionally keeping entire module-level objects alive
- Using a Map (strong keys) instead of WeakMap when you want the entries to expire with the key objects Map holds keys strongly and will never auto-remove entries
Note
The most effective way to find memory leaks is the Chrome DevTools Memory panel. Take a heap snapshot before a suspected leak operation, perform the operation, force GC (the trash icon), then take another snapshot. Use the "Comparison" view to see what was added. Sort by "Retained Size" to find the largest offenders.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4