Difficulty: Advanced
JavaScript is a garbage-collected language, meaning developers do not manually allocate and free memory as they would in C or C++. However, understanding how memory works under the hood is crucial for writing performant applications and avoiding memory leaks that can degrade performance or crash long-running applications like SPAs and Node.js servers.
Memory in JavaScript is divided into two main areas: the stack and the heap. The stack stores primitive values (numbers, strings, booleans, undefined, null, symbols, bigints) and function call frames. It operates in a last-in-first-out manner and is extremely fast. The heap stores objects, arrays, functions, and other reference types. Variables on the stack hold references (pointers) to objects on the heap. When you assign an object to a new variable, both variables point to the same heap location - this is why objects are shared by reference.
The primary garbage collection algorithm in modern JavaScript engines (V8, SpiderMonkey, JavaScriptCore) is mark-and-sweep. The garbage collector starts from root references (global object, currently executing function's local variables, closures) and traverses all reachable objects, marking them as alive. Any object not reachable from any root is considered garbage and its memory is reclaimed. An older algorithm, reference counting, tracked how many references pointed to each object and freed it when the count reached zero. Reference counting fails with circular references (A references B which references A), which is why mark-and-sweep replaced it.
Memory leaks occur when objects that are no longer needed remain reachable. The most common sources are: accidental global variables (assigning to undeclared variables in non-strict mode), forgotten timers and intervals (setInterval callbacks holding references), detached DOM nodes (removing an element from the DOM while a JavaScript variable still references it), and closures that capture large scopes unintentionally. Each of these keeps objects in memory longer than necessary, causing the heap to grow over time.
ES2021 introduced WeakRef and FinalizationRegistry for advanced memory management. A WeakRef holds a weak reference to an object that does not prevent garbage collection. You can dereference it with .deref(), which returns the object if it still exists or undefined if it has been collected. FinalizationRegistry lets you register a callback that runs after an object is garbage collected. These are low-level tools intended for caches and resource management, not everyday code.
Chrome DevTools provides excellent memory profiling tools. The Memory tab lets you take heap snapshots, record allocation timelines, and compare snapshots to identify retained objects. The Performance tab shows garbage collection pauses. To diagnose a leak: take a snapshot, perform the suspected leaking action, take another snapshot, and compare - objects that grow but should not are likely leaks.
// Primitives - stored on the stack (copied by value)
let a = 10;
let b = a; // independent copy
b = 20;
console.log(a, b);
// Objects - stored on the heap (copied by reference)
let obj1 = { x: 1 };
let obj2 = obj1; // both point to same heap object
obj2.x = 99;
console.log(obj1.x, obj2.x);
// Proving reference sharing
console.log(obj1 === obj2);
Primitives are independent copies on the stack. Objects are references to the same heap location, so mutating through one variable affects all references.
// 1. Accidental global (non-strict mode)
function leakyGlobal() {
// 'use strict'; // uncomment to catch this error
leaked = 'I am a global variable now!';
}
// 2. Forgotten timer
function startLeakyTimer() {
const bigData = new Array(1000000).fill('x');
const id = setInterval(() => {
// bigData is captured in closure - never freed
if (bigData.length > 0) {
console.log('Timer running...');
}
}, 1000);
// Fix: store id and call clearInterval(id) when done
return id;
}
// 3. Detached DOM node
function domLeak() {
const el = document.createElement('div');
document.body.appendChild(el);
const ref = el; // strong reference
document.body.removeChild(el);
// 'ref' still holds the detached DOM node in memory
// Fix: set ref = null after removal
}
// 4. Closure holding a large scope
function closureLeak() {
const hugeArray = new Array(1000000).fill(0);
return function() {
// This closure captures the entire scope including hugeArray
return 'I only need this string';
};
}
Each example shows a different memory leak pattern. Accidental globals persist forever. Forgotten timers keep closure variables alive. Detached DOM nodes stay in memory if referenced. Closures capture their entire enclosing scope.
// WeakRef - does not prevent garbage collection
let bigObject = { data: new Array(10000).fill('important') };
const weakRef = new WeakRef(bigObject);
console.log(weakRef.deref()?.data.length); // 10000
// If we remove the strong reference, the object can be GC'd
bigObject = null;
// After GC runs, weakRef.deref() returns undefined
// (Cannot force GC in user code - this is engine-dependent)
// FinalizationRegistry - callback after GC
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with id "${heldValue}" was garbage collected`);
});
let obj = { name: 'temporary' };
registry.register(obj, 'temp-obj-1');
// When obj is GC'd, the callback fires with 'temp-obj-1'
console.log('WeakRef and FinalizationRegistry registered');
WeakRef lets you hold a reference that does not prevent garbage collection. FinalizationRegistry triggers a cleanup callback after an object is collected. Both are useful for caches and resource management.
Memory Allocation, Garbage Collection, Mark-and-Sweep, Memory Leaks, WeakRef, FinalizationRegistry, DevTools