Stack vs Heap — Where Variables Live
JavaScript allocates memory in two regions. Understanding which region holds which data is the foundation for reasoning about performance and leaks.
What Keeps Objects Alive — The Retain Graph
The garbage collector starts from a set of GC roots (global object, active call stack, V8 internals) and marks every object reachable by following references. Anything not marked is unreachable and can be freed. An object is kept alive by a single live reference anywhere in the graph.
// Object is reachable → NOT collected
const cache = new Map();
function store(key, value) {
cache.set(key, value); // cache lives as long as the module
}
// Object is unreachable → eligible for GC
function example() {
const temp = { data: new ArrayBuffer(1_000_000) };
// temp goes out of scope here → GC can reclaim it
}Common Memory Leak Patterns
Leaks in JavaScript are almost always caused by unintentional long-lived references — the GC cannot collect something you are still holding onto. The five patterns below account for the vast majority of real-world leaks.
// ❌ Leak: listener never removed
function attachHandler(element) {
element.addEventListener("click", handleClick);
// handleClick holds a closure over outer variables
// element stays alive as long as handleClick is referenced
}
// ✅ Fix: always remove listeners when no longer needed
function attachHandler(element) {
const controller = new AbortController();
element.addEventListener("click", handleClick, {
signal: controller.signal,
});
return () => controller.abort(); // call this to clean up
}
// ✅ React fix: cleanup in useEffect return
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);WeakMap, WeakSet, WeakRef, FinalizationRegistry
ES2015+ added weak collection types that hold references without preventing garbage collection. They are ideal for metadata stores and caches where the lifetime of the value should follow the lifetime of the key object.
| Map | WeakMap |
|---|---|
| Keys can be any value | Keys must be objects or non-registered symbols |
| Strongly holds keys | Weakly holds keys — GC can collect them |
| Iterable (forEach, for…of) | Not iterable — no size, no forEach |
| Has .size property | No .size — count unknown at runtime |
| Use for general caches | Use for object-keyed metadata, private data |
// WeakMap — keys are weakly held; entry removed when key is GC'd
const metadata = new WeakMap();
class Component {
constructor() {
metadata.set(this, { createdAt: Date.now() });
}
// When this instance is collected, metadata entry is removed automatically
}
// WeakRef — hold a reference that doesn't prevent GC
const ref = new WeakRef(largeObject);
const deref = ref.deref(); // undefined if already collected
if (deref) {
deref.doWork();
}
// FinalizationRegistry — callback when target is collected
const registry = new FinalizationRegistry((heldValue) => {
console.log("Collected:", heldValue);
});
registry.register(target, "my-target");Heap Snapshots in DevTools
Chrome DevTools Memory panel lets you take heap snapshots and compare them to find retained objects. This is the primary tool for diagnosing suspected memory leaks.
Allocation Timeline — Spotting the Leak Shape
The Allocation instrumentation timeline records every allocation over time. The shape of the heap graph tells you immediately whether a leak is present.
React-Specific Memory Patterns
React components mount and unmount frequently. Any subscription, timer, or network request started in useEffect must be cancelled in the cleanup function, otherwise it outlives the component and leaks.
// ✅ AbortController for fetch — cancels inflight request on unmount
useEffect(() => {
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then((res) => res.json())
.then(setData)
.catch((err) => {
if (err.name !== "AbortError") setError(err);
});
return () => controller.abort();
}, []);
// ✅ Clear timer on unmount
useEffect(() => {
const id = setTimeout(() => setVisible(false), 3000);
return () => clearTimeout(id);
}, []);
// ✅ Remove event listener on unmount
useEffect(() => {
const handler = (e) => setKey(e.key);
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);