Inside the Heap: Where JavaScript Stores Everything That Matters
A deep dive into V8's heap memory, where objects, closures, promises, and everything dynamic lives. Understanding what goes in the heap and why.
In the last post, we explored the stack, the region of memory that handles execution. Now we turn to its counterpart: the heap, where data persists beyond function calls.
This is where JavaScript stores things that:
- Don’t have a fixed size
- Can outlive a single function call
- Need to grow, shrink, or be shared
In Node.js (via the V8 engine), the heap is where the interesting stuff lives. Objects, closures, promises, anything that makes JavaScript feel dynamic ends up here.
Let’s walk through what actually goes into the heap and why.
Objects
Any JavaScript object, no matter how small, lives in the heap.
let obj = { a: 1, b: 2 }; // object → heap
Even if an object has a single property, it still goes to the heap.
Why?
Because objects are dynamic. Properties can be added, removed, reordered, or mutated at runtime. The stack cannot handle that kind of flexibility.
Internally, V8 uses hidden classes (also called shapes) to track object structure and optimize property lookups. But regardless of these optimizations, the object still lives in the heap.
Objects live in the heap. Always.
Arrays
Arrays are objects with a numeric indexing convention.
let arr = [1, 2, 3];
Even though the elements are primitives, the array itself lives in the heap.
Important nuance:
- The array structure is heap-allocated
- The elements may be stored inline or separately depending on V8 optimizations
- Dense arrays, sparse arrays, and mixed-type arrays are handled differently
But from a mental-model perspective: if it’s an array, it’s in the heap.
Functions (as Objects)
Functions in JavaScript are not just executable code. They are full-fledged objects.
A function carries:
- Executable bytecode
- A
.prototypeobject - Metadata
- Possibly a closure
function greet() {}
const x = greet;
What’s happening:
greetis a heap-allocated function objectxis just a reference pointing to it
Every function (named, anonymous, arrow) lives in the heap.
This is why functions can:
- Be passed around
- Be stored in variables
- Be returned from other functions
- Carry state
Execution uses the stack. Functions themselves live in the heap.
Closures
Closures are where heap behavior becomes visible.
A closure is a function bundled together with variables from its lexical environment that it still needs access to.
function outer() {
let count = 0; // primitive
return function inner() {
count++;
return count;
}
}
What’s interesting here is what happens to count.
Normally, count would live in the stack frame of outer. But inner() still needs it after outer() has returned.
So V8 does something clever:
- It creates a heap-allocated context object
countis stored in that context instead of the stack frameinner()holds a reference to that context
This is why closures can retain state indefinitely.
This is also why closures are one of the most common sources of accidental memory retention.
Buffers
Node.js Buffer objects are heap-allocated structures that wrap raw binary data.
const buf = Buffer.alloc(1024);
Under the hood:
- The Buffer object itself lives in V8’s heap
- The binary data may be stored in V8’s heap (for small buffers) or in external memory (for large buffers)
- V8 tracks both to ensure proper cleanup
This design balances performance with memory safety. Buffers are powerful because they bridge managed and unmanaged memory, but that power requires careful handling.
Promises, Maps, Sets, Errors
All higher-level abstractions are heap-based.
This includes:
PromiseMapSetWeakMapWeakSet- Error objects
Even a simple Promise:
new Promise(() => {});
creates heap objects for the promise itself and its internal state.
Heap usage grows not because JavaScript is slow, but because these abstractions are real, heap-allocated objects.
Why the Heap Exists at All
The heap exists because JavaScript allows:
- Objects to grow dynamically
- Data to outlive function calls
- Shared mutable state
- Asynchronous execution
- Closures and higher-order functions
None of this is compatible with stack-only memory.
The heap is slower than the stack, yes. But it enables the language. So when should you expect something to live in the heap?
The Rule of Thumb
Here’s the mental shortcut that actually works:
If it can change shape, grow, shrink, or outlive a function call, it belongs in the heap.
The stack handles execution. The heap handles existence.
The heap is what makes JavaScript expressive. But it comes with a problem.
The stack cleans itself up automatically. The heap doesn’t. Objects stick around. Closures hold references. Data piles up.
So how does V8 know when it’s safe to free memory? How does it decide what’s garbage and what’s still needed?
That’s the job of garbage collection—and that’s exactly what we cover in Garbage Collection in V8: How Memory Is Actually Reclaimed.