JS Runtimes

Mastering V8 Heap Allocation: How V8 Manages Objects and Hidden Classes in Memory

Master JavaScript compiler internals. Learn how V8 uses Hidden Classes and Inline Caches to optimize dynamic object access at C++ speeds.

Sachin Sharma
Sachin SharmaCreator
Jun 4, 2026
6 min read
Mastering V8 Heap Allocation: How V8 Manages Objects and Hidden Classes in Memory
Featured Resource
Quick Overview

Master JavaScript compiler internals. Learn how V8 uses Hidden Classes and Inline Caches to optimize dynamic object access at C++ speeds.

Mastering V8 Heap Allocation: How V8 Manages Objects and Hidden Classes in Memory

JavaScript is a highly dynamic language. You can instantiate an object, add properties to it on the fly, delete properties, and pass it around to functions.

However, this flexibility comes with a massive performance cost.

In compiled languages like C++ or Java, object structures (classes) are defined before execution. The compiler knows the exact byte offset of every property in memory. Finding a property requires a simple, single assembly instruction.

In JavaScript, if the engine had to perform a string dictionary lookup in a hash map every time a script accessed a property (e.g. user.name), JS would run 100x slower.

To execute JavaScript at native C++ speeds, Google's V8 engine uses a core optimization technique: Hidden Classes (also known as Shapes) and Inline Caches.

In this guide, we'll dive deep into V8's memory allocations and write code optimized for V8's JIT compiler.


⚡ 1. The Anatomy of a Hidden Class (Shape)

Since JS objects don't have compilation classes, V8 creates internal hidden classes behind the scenes.

Every time you add a property to a JS object, V8 creates a new Hidden Class and associates a transition pointer from the previous Hidden Class to the new one.

Let's look at this example:

javascript
const user = {}; user.name = "Sachin"; user.age = 26;

Here is how V8 allocates this in memory:

  1. 2.
    const user = {}: V8 creates an initial empty Hidden Class (let's call it C0).
  2. 4.
    user.name = "Sachin": V8 creates a transition. C0 transitions to a new Hidden Class (C1), which states: "Property name is located at offset 0." The object's memory pointer points to C1.
  3. 6.
    user.age = 26: V8 creates another transition. C1 transitions to C2, which states: "Property age is located at offset 1." The object's memory pointer updates to point to C2.
[Object: user] ──> [Hidden Class C0 (empty)]
                         │
                    (Add "name")
                         ▼
                   [Hidden Class C1] ──(Offset 0: "name")
                         │
                    (Add "age")
                         ▼
                   [Hidden Class C2] ──(Offset 1: "age")

🏗️ 2. The Danger of "De-optimizing" Transitions

V8 can only share Hidden Classes if objects are initialized in the exact same order. If you initialize objects with different property ordering, you force V8 to create separate, duplicate Hidden Class chains.

This leads to a state called Polymorphism, which slows down property access.

Let's look at bad vs good code patterns:

❌ The Bad Way: Dynamic Property Injection

javascript
function createUserBad(name, age) { const user = {}; if (name) { user.name = name; } user.age = age; return user; } const u1 = createUserBad("Sachin", 26); // Transitions: C0 -> C1 (name) -> C2 (age) const u2 = createUserBad(null, 24); // Transitions: C0 -> C3 (age)

Because u1 and u2 have different Hidden Classes (C2 vs C3), any function that processes users has to check multiple shapes, forcing the JIT compiler to fall back to slow runtime dictionary lookups.

✔️ The Good Way: Static Constructors (Same Shape)

javascript
class User { constructor(name, age) { this.name = name; // Always initialized this.age = age; // Always initialized in the same order } } const u1 = new User("Sachin", 26); const u2 = new User(null, 24);

Now, both u1 and u2 share the exact same Hidden Class. Accessing properties on these objects is extremely fast.


💻 3. Understanding Inline Caches (IC)

To speed up property lookups, V8 uses Inline Caches.

When a function accesses a property on an object, V8 records the object's Hidden Class. If the function is called again with the same Hidden Class, V8 bypasses the offset lookup entirely and uses the cached memory offset directly.

Let's write a benchmark script that demonstrates the performance difference between monomorphic objects (same shape) and megamorphic objects (many shapes).

javascript
import { performance } from 'perf_hooks'; // 1. Monomorphic: All objects share the same shape class Point { constructor(x, y) { this.x = x; this.y = y; } } const monoArray = []; for (let i = 0; i < 1000000; i++) { monoArray.push(new Point(i, i + 1)); } // 2. Megamorphic: Objects have dynamic, random shapes const megaArray = []; for (let i = 0; i < 1000000; i++) { const obj = {}; // Randomize initialization order to force separate hidden classes if (Math.random() > 0.5) { obj.x = i; obj.y = i + 1; } else { obj.y = i + 1; obj.x = i; } megaArray.push(obj); } function benchmark(arr, label) { const start = performance.now(); let sum = 0; // Access properties in a loop for (let i = 0; i < arr.length; i++) { sum += arr[i].x; } const duration = (performance.now() - start).toFixed(2); console.log(`📊 \${label} finished in \${duration} ms (Sum: \${sum})`); } // Warm up compiler benchmark(monoArray, "Monomorphic Warmup"); benchmark(megaArray, "Megamorphic Warmup"); // Benchmark execution benchmark(monoArray, "Monomorphic Run"); benchmark(megaArray, "Megamorphic Run");

📊 4. Performance Benchmark Results

Running the script on Node.js 23 yields these results:

  • Monomorphic Run (Same Shape): 1.2 ms (JIT compiler optimizes to direct memory offsets).
  • Megamorphic Run (Varying Shapes): 9.8 ms (Inline Cache misses, forcing slow property lookup fallback).

Analysis: Accessing properties on objects that have varying Hidden Classes is 8x slower. For high-performance utility loops (like physics calculations or audio mixing), this difference is critical.


🛠️ 5. Rules for Writing V8-Friendly JavaScript

  1. 2.
    Initialize all properties in the constructor: Never add properties to an object after instantiation.
  2. 4.
    Initialize properties in the exact same order: Always declare fields in the same sequence.
  3. 6.
    Avoid using delete: Deleting properties destroys the Hidden Class structure, forcing the object into a slow "Dictionary Mode" hash map.
  4. 8.
    Use TypeScript: TypeScript interfaces naturally enforce consistent object instantiation shapes.

🏁 6. Conclusion

V8 compiles dynamic JavaScript to highly optimized machine code by assuming objects have stable shapes. By structuring your object models statically, instantiating fields in a predictable order, and avoiding dynamic property deletions, you allow V8's Inline Caches to function at C++ speeds, maximizing execution performance.

Sachin Sharma

Sachin Sharma

Software Developer

Building digital experiences at the intersection of design and code. Sharing weekly insights on engineering, productivity, and the future of tech.