Garbage Collection
JavaScript manages memory for you: you allocate by creating values, and the engine automatically frees memory that is no longer in use. The mechanism behind this is the garbage collector (GC), and the single concept it relies on is reachability. Understanding how the GC decides what to keep — and what keeps your objects alive longer than intended — is the difference between code that runs lean and code that slowly leaks memory until the tab or process falls over.
Reachability: the core idea
A value is reachable if it can still be accessed somehow from the running program. The engine starts from a set of inherently reachable values called the roots — the global object (window in browsers, globalThis in Node), the currently executing function’s local variables and parameters, and the chain of functions that called it. Any value reachable from a root, by following references, is also reachable. Everything else is garbage and may be reclaimed.
let user = { name: "Ada" };
// The object { name: "Ada" } is reachable via `user`.
user = null;
// Nothing references the object anymore — it becomes garbage
// and will be collected at some point.
The key word is some point. You never call the collector directly; the engine runs it on its own schedule. You only control which references exist.
Mark-and-sweep
Modern engines (V8 in Chrome and Node, SpiderMonkey in Firefox, JavaScriptCore in Safari) use a mark-and-sweep algorithm. It works in two phases:
- Mark — Starting from the roots, the GC walks every reference and marks each reachable object as “in use.”
- Sweep — It scans the heap and frees any object that was not marked.
Because marking follows references transitively, even complex object graphs and cycles are handled correctly. Two objects that reference each other but are unreachable from any root are still collected — they simply never get marked.
function makePair() {
const a = {};
const b = {};
a.partner = b; // cycle: a -> b
b.partner = a; // cycle: b -> a
return null; // neither a nor b is returned or stored
}
makePair();
// a and b reference each other, but nothing outside reaches them.
// Mark-and-sweep collects both; reference counting could not.
Reachability — not reference counting — is what JavaScript uses. This is why circular references do not leak in JS, unlike in some older systems.
V8 further optimizes this with a generational collector: new objects live in a small “young generation” that is collected often and quickly, while objects that survive get promoted to an “old generation” collected less frequently. This matches the observation that most objects die young.
Common sources of memory leaks
A “leak” in a GC language doesn’t mean memory vanishes — it means you are unintentionally keeping objects reachable so the GC can never free them. Here are the usual culprits.
Lingering references
Forgotten entries in long-lived objects, arrays, or caches keep their contents alive forever.
const cache = {};
function process(id, data) {
cache[id] = data; // never deleted — grows without bound
return data.length;
}
If nothing ever removes old keys, cache pins every data value for the lifetime of the program. A Map with a deliberate eviction policy — or a WeakMap — is the fix.
Forgotten timers and listeners
Active timers and event listeners are roots in their own right: the callback (and everything it closes over) stays reachable until you stop them.
const widget = { huge: new Array(1_000_000).fill("x") };
const id = setInterval(() => {
console.log(widget.huge.length);
}, 1000);
// Forgetting this line leaks `widget` forever:
clearInterval(id);
The same applies to addEventListener. Always pair it with removeEventListener, or pass an AbortSignal to detach in one shot:
const controller = new AbortController();
button.addEventListener("click", onClick, { signal: controller.signal });
// Later, when the component is destroyed:
controller.abort(); // removes the listener and frees its captures
Closures that capture too much
A closure keeps its entire enclosing scope alive. A long-lived closure that only needs one small field but closes over a large object pins the whole thing.
function attach(element) {
const bigData = new Array(1_000_000).fill(0);
const size = bigData.length; // extract what you need
element.onclick = () => console.log(size); // closes over `size`, not bigData
}
Detached DOM nodes
If you remove a node from the document but still hold a JavaScript reference to it, the node — and its whole subtree — cannot be collected.
let detached = document.getElementById("panel");
detached.remove(); // gone from the page...
// ...but still referenced here, so it stays in memory.
detached = null; // now it can be collected
WeakMap, WeakSet, and weak references
When you want to associate data with an object without preventing that object from being collected, use WeakMap or WeakSet. Their keys are held weakly: if the only remaining reference to a key is inside the weak collection, the entry disappears automatically.
| Structure | Keys held | Iterable | Typical use |
|---|---|---|---|
Map | Strongly | Yes | General key/value storage |
WeakMap | Weakly | No | Metadata keyed by object, auto-cleaned |
WeakSet | Weakly | No | ”Have I seen this object?” tracking |
const metadata = new WeakMap();
function tag(obj) {
metadata.set(obj, { taggedAt: Date.now() });
}
let node = { id: 1 };
tag(node);
node = null; // the WeakMap entry is now eligible for GC automatically
Best practices
- Null out or delete references you no longer need, especially in long-lived objects and module-level caches.
- Always clear timers (
clearInterval/clearTimeout) and remove listeners — anAbortControllermakes teardown a single call. - Keep closures lean: capture the specific value you need, not a large enclosing object.
- Drop references to removed DOM nodes so detached subtrees can be reclaimed.
- Use
WeakMap/WeakSetfor object-keyed metadata you want collected alongside the object. - Give caches a bounded size or an eviction strategy instead of letting them grow forever.
- Profile real usage with the browser DevTools Memory panel (heap snapshots) or Node’s
--inspectrather than guessing.