Skip to main content
๐Ÿ”งadvanced

Memory Management

JavaScript manages memory automatically through garbage collection. Learn how the heap and stack work, what causes memory leaks, and how to profile and fix them.

Memory in JavaScript

JavaScript automatically allocates and frees memory โ€” you don't use malloc/free like in C. But understanding memory helps you avoid memory leaks and write efficient code.

Two main memory locations:

StackHeap
StoresPrimitives, references, call framesObjects, arrays, functions
SizeFixed, smallDynamic, large
AccessVery fast (LIFO)Slower (pointer dereference)
LifetimeAutomatic (frame exit)Managed by GC

The Stack

The stack holds:

  • Primitive values (number, boolean, string, null, undefined, symbol, bigint)
  • References (pointers) to heap objects
  • Execution context frames
hljs javascript
function example() {
  const a = 42;        // number โ†’ stored on stack
  const b = "hello";   // string โ†’ stack (small strings may be interned)
  const c = { x: 1 };  // reference on stack, object on heap

  // Stack frame for example() is added
  // When example() returns, frame (and a, b, c) are removed
}

The Heap

The heap stores all objects, arrays, and functions. Memory here persists until the garbage collector reclaims it.

hljs javascript
const obj = { name: "Alice" }; // { name: "Alice" } lives on the heap
                                // obj (the reference) is on the stack

Garbage Collection

V8 uses a generational garbage collector:

Young Generation (Nursery)

Most objects are short-lived. New objects start here:

  • Small space (~1-8MB)
  • Collected frequently using Scavenge (copying GC)
  • Survivors are promoted to old generation after 2 collections
hljs javascript
// These temporary objects live briefly in young gen:
function processData(items) {
  return items.map(x => ({ value: x * 2 })); // temporary objects
}
// After processData returns, these objects become unreachable โ†’ GC reclaims

Old Generation

Long-lived objects (survived young gen twice) move here:

  • Larger space
  • Collected with Mark-and-Compact (full GC) โ€” more expensive but less frequent
  • V8 runs these incrementally to avoid long pauses

Mark-and-Sweep

The fundamental GC algorithm:

  1. Mark phase: Starting from "roots" (global variables, stack variables), trace all reachable objects and mark them
  2. Sweep phase: Reclaim memory of unmarked (unreachable) objects
hljs javascript
let obj1 = { name: "Alice" };  // reachable via obj1
let obj2 = { name: "Bob" };    // reachable via obj2

obj1 = null; // { name: "Alice" } is now unreachable โ†’ eligible for GC

Memory Leaks

A memory leak is memory that's allocated but never freed โ€” even though you're done with it. Common causes:

1. Forgotten Event Listeners

hljs javascript
// โœ— Leak: adding listeners without removing them
function setupUI() {
  const handler = () => console.log("click");
  document.getElementById("btn").addEventListener("click", handler);
  // if this function is called many times, listeners pile up!
}

// โœ“ Remove when done
function setupUI() {
  const handler = () => console.log("click");
  const btn = document.getElementById("btn");
  btn.addEventListener("click", handler);

  // Return cleanup function
  return () => btn.removeEventListener("click", handler);
}

2. Closures Holding Large References

hljs javascript
// โœ— The closure captures a large array unnecessarily
function processLargeArray() {
  const largeData = new Array(1_000_000).fill({ x: Math.random() });

  return function() {
    // Only needs one value, but the entire array stays in memory
    // because the closure references `largeData`
    return largeData[0];
  };
}

// โœ“ Extract only what you need before closing
function processLargeArray() {
  const largeData = new Array(1_000_000).fill({ x: Math.random() });
  const firstItem = largeData[0]; // extract
  // largeData can now be GC'd after this function returns

  return function() {
    return firstItem; // closure only captures firstItem
  };
}

3. Global Variables

hljs javascript
// โœ— Accidentally global (missing var/let/const)
function createUser() {
  userData = { name: "Alice" }; // no declaration! becomes global
}

// โœ“ Always declare
function createUser() {
  const userData = { name: "Alice" };
}

4. Detached DOM Nodes

hljs javascript
// โœ— Reference to removed DOM element
let detachedNode;
function createLeak() {
  const node = document.createElement("div");
  document.body.appendChild(node);
  detachedNode = node; // hold reference
  document.body.removeChild(node); // remove from DOM
  // node is removed from DOM but detachedNode still references it!
}

// โœ“ Clean up references
function noLeak() {
  const node = document.createElement("div");
  document.body.appendChild(node);
  document.body.removeChild(node);
  // No reference held โ†’ GC can reclaim
}

5. Caches Without Limits

hljs javascript
// โœ— Unbounded cache grows forever
const cache = {};
function memoize(fn) {
  return function(key) {
    if (!cache[key]) {
      cache[key] = fn(key); // memory grows indefinitely
    }
    return cache[key];
  };
}

// โœ“ Use WeakMap (or LRU cache with size limit)
const weakCache = new WeakMap();
function memoizeObject(fn) {
  return function(obj) { // obj must be a reference type
    if (!weakCache.has(obj)) {
      weakCache.set(obj, fn(obj));
    }
    return weakCache.get(obj);
    // When obj is no longer referenced, WeakMap entry is GC'd automatically
  };
}

WeakMap and WeakSet

WeakMap and WeakSet hold weak references โ€” they don't prevent GC:

hljs javascript
const wm = new WeakMap();

let user = { name: "Alice" };
wm.set(user, { sessions: 5 }); // user โ†’ data

user = null; // user object becomes unreachable
// WeakMap entry is automatically cleaned up โ€” no memory leak!

// Regular Map would keep the entry alive:
const m = new Map();
let obj = {};
m.set(obj, "data");
obj = null; // Map still holds a reference โ€” obj is NOT GC'd!

Profiling Memory in DevTools

In Chrome DevTools โ†’ Memory tab:

  • Heap Snapshot โ€” see what's currently in memory
  • Allocation instrumentation โ€” track allocations over time
  • Allocation sampling โ€” lightweight profiling
hljs javascript
// To find leaks:
// 1. Take a heap snapshot (baseline)
// 2. Perform the action suspected of leaking (e.g., open/close a modal)
// 3. Take another heap snapshot
// 4. Compare snapshots โ€” objects that grew indicate a leak
โ–ถTry it yourself

Key Takeaways

  • Stack: primitives + references, fast, auto-freed when function returns
  • Heap: objects + arrays + functions, managed by GC
  • V8's GC is generational: young gen (frequent, fast) + old gen (infrequent, thorough)
  • Mark-and-sweep: trace reachability from roots, reclaim unreachable objects
  • Common memory leaks: forgotten event listeners, closures over large data, globals, detached DOM nodes, unbounded caches
  • Use WeakMap/WeakSet for caches โ€” entries are GC'd when keys are unreachable

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’