Memory management is largely automatic in JavaScript. You allocate memory by creating objects, arrays, and closures, and the runtime reclaims that memory when it is no longer needed — a process called garbage collection (GC). Understanding how the garbage collector works helps you write code that is less likely to leak memory and more likely to interact well with the runtime’s optimisations.
The memory lifecycle
Every value in JavaScript goes through three phases:
- Allocation — memory is reserved when you create a value (
const obj = {}, "hello", [1, 2, 3]).
- Use — the value is read and written.
- Release — when the value is no longer reachable, the GC reclaims its memory.
Primitive values (numbers, booleans, null, undefined) are usually small enough to be stored inline or on the stack and are freed as soon as their scope ends. Objects, arrays, and closures live on the heap and require the garbage collector’s involvement.
Reachability
The core concept in modern garbage collection is reachability. A value is reachable if it can be accessed in any way from the current execution — through the call stack, global variables, closures, DOM references, or chains of object properties. A value that cannot be reached from anywhere is considered garbage and is eligible for collection.
The roots of the reachability graph are:
- Currently executing functions and their local variables.
- Global variables and the global object.
- The call stack itself.
From each root, the collector follows all references. Anything reachable from a root is kept alive. Everything else is collected.
Mark-and-sweep
The algorithm that all modern JavaScript engines use (including V8’s major GC) is called mark-and-sweep. It runs in two phases:
Mark phase
Starting from the roots, the collector traverses every reachable object and marks it as “alive”. It follows every reference — object properties, array elements, closure variables — recursively, until all reachable objects are marked.
Sweep phase
The collector scans the entire heap. Any object that was not marked is unreachable and its memory is freed. The freed space is made available for future allocations.
let user = { name: "Alice", age: 30 };
// { name: "Alice" } is reachable via `user`
user = null;
// The reference is severed. If nothing else refers to { name: "Alice" },
// it becomes unreachable and will be collected on the next GC cycle.
Generational collection
In practice, V8 uses a generational strategy. The heap is divided into:
- Young generation (nursery) — newly allocated objects live here. Most objects die young, so this area is collected frequently and cheaply using a fast algorithm called Scavenge.
- Old generation — objects that survive several collections in the young generation are promoted here. This area is collected less frequently using a full mark-and-sweep-compact cycle.
This optimisation is based on the empirical observation that most objects in real programs have very short lifetimes.
V8 also runs garbage collection incrementally and concurrently, spreading GC work across multiple small pauses rather than stopping the world for a single long pause. This keeps application latency low.
Reference counting
An older algorithm is reference counting: each object keeps a count of how many references point to it. When the count drops to zero, the object is freed immediately.
Reference counting has an appealing simplicity, but it has a fatal flaw: circular references.
function createCycle() {
const a = {};
const b = {};
a.other = b; // a references b
b.other = a; // b references a
// When createCycle returns, a and b are unreachable from outside,
// but each still has a reference count of 1 (from the other).
// A reference-counting GC would never free them.
}
createCycle();
Modern JavaScript engines do not use reference counting as the primary GC algorithm for this reason. Some environments historically used it (Internet Explorer’s DOM used a COM-based reference-counting system, leading to notorious memory leaks when mixing DOM nodes and JS objects).
Memory leaks
A memory leak occurs when objects that are no longer needed stay reachable and cannot be collected. Even with a sophisticated GC, leaks are possible because the collector can only collect what is unreachable — and it is your code that controls reachability.
Accidental global variables
Accidentally creating global variables is one of the most common causes of memory leaks. Without "use strict" or a module system, assigning to an undeclared variable creates a global property that persists for the entire lifetime of the page.
function process() {
// Forgot `let` or `const` — `result` becomes window.result in a browser
result = computeHeavyData();
}
Use "use strict" or ES modules (which are strict by default) to turn undeclared variable assignments into errors.
Forgotten timers and callbacks
A setInterval callback holds a reference to everything in its closure. If the interval is never cleared, those objects are never collected.
const data = fetchLargeDataset();
const intervalId = setInterval(function () {
updateDisplay(data); // `data` is held alive by the closure
}, 1000);
// If you navigate away or no longer need the updates:
clearInterval(intervalId); // must do this explicitly
The same applies to event listeners added to DOM nodes. If you add a listener but never remove it, the callback (and everything it closes over) stays in memory even if the relevant UI has been removed from the page.
function attachHandler() {
const heavyObject = { /* large data */ };
document.getElementById("button").addEventListener("click", function () {
console.log(heavyObject); // heavyObject cannot be collected
});
}
// Better: remove the listener when no longer needed
const button = document.getElementById("button");
function handler() {
console.log("clicked");
}
button.addEventListener("click", handler);
// Later:
button.removeEventListener("click", handler);
Detached DOM nodes
Keeping a JavaScript reference to a DOM node that has been removed from the document prevents both the node and its subtree from being garbage collected.
let detachedTree;
function createDetachedNode() {
const ul = document.createElement("ul");
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
ul.appendChild(li);
}
detachedTree = ul; // global reference keeps the entire subtree alive
}
createDetachedNode();
document.body.appendChild(detachedTree);
// After removal from DOM:
document.body.removeChild(detachedTree);
// detachedTree is still referenced globally — none of it is collected
detachedTree = null; // now it can be collected
Closures capturing large scopes
function outer() {
const largeArray = new Array(1_000_000).fill("x");
return function inner() {
// inner only uses a tiny part of outer's scope,
// but the entire largeArray is kept alive as long as inner is reachable
return largeArray[0];
};
}
const fn = outer(); // largeArray cannot be collected until fn is released
If inner only needs a small piece of the data, extract that piece explicitly rather than closing over the entire large structure.
WeakMap and WeakSet
ES2015 introduced WeakMap and WeakSet for situations where you want to associate data with an object without preventing the GC from collecting that object. Keys in a WeakMap (and items in a WeakSet) are held weakly — they do not prevent garbage collection.
const cache = new WeakMap();
function process(domNode) {
if (cache.has(domNode)) {
return cache.get(domNode);
}
const result = expensiveComputation(domNode);
cache.set(domNode, result);
return result;
}
// When domNode is removed from the DOM and no other JS holds a reference to it,
// the WeakMap entry is automatically removed and memory is freed.
Use WeakMap when you need to attach private metadata to objects owned by other code (such as DOM nodes or third-party objects). The metadata will automatically disappear when the object is collected, preventing leaks.
Best practices
- Always clear intervals and timeouts when they are no longer needed (
clearInterval, clearTimeout).
- Remove event listeners when the element they are attached to is destroyed or when the feature is disabled.
- Null out references to large objects when you are done with them, especially in long-lived closures.
- Prefer
WeakMap / WeakSet for caching or annotating objects you do not own.
- Use browser DevTools (Chrome Memory tab) to take heap snapshots and identify objects that should have been collected but were not.
- Enable strict mode (
"use strict" or use ES modules) to avoid accidental globals.