Chapter 2.6☕ 22 min read

Hidden Classes, Inline Caches & V8 Optimization

V8 speed deta hai — par condition hai. Shape change mat karo bhai!

01Hidden Classes (Shapes/Maps)

A hidden class is V8's internal representation of an object's structure. Every object has a hidden class — also called Shape or Map in V8 source code.

Two objects with the same properties in the same ORDER share the same hidden class. The hidden class tracks: property names, property types, and property offsets. This allows V8 to access properties at a fixed memory offset — just like C struct access.

When you add or delete a property, V8 transitions to a new hidden class. Objects with the same hidden class can be optimized together by TurboFan.

// Two objects with SAME hidden class — V8 can optimize both
const p1 = { x: 1, y: 2 }; // Hidden class: HClass{x,y}
const p2 = { x: 5, y: 8 }; // Same! HClass{x,y} — shared
// V8 knows: x is at offset 0, y is at offset 1 — fast access!

// Property ORDER matters — different order = different hidden class!
const p3 = { y: 1, x: 2 }; // Different! HClass{y,x} — NOT shared with p1/p2

// Adding property after creation = NEW hidden class
const p4 = { x: 0 };       // HClass{x}
p4.y = 1;                   // Transition → HClass{x,y}
// p4 now has same class as p1/p2, but WENT THROUGH 2 classes

// Deleting property also transitions hidden class
delete p1.y;                // HClass{x} — NEW class, different from p4 after delete
Hidden classes are why V8 can access obj.x as fast as C's struct.x — it's literally an offset into a memory block. But this only works when the shape is stable. Every shape change forces a transition, invalidating optimized code that assumed the old shape.
02Inline Caches (ICs)

An Inline Cache (IC) is a fast lookup mechanism V8 inserts at every property access site in compiled code.

At each property access (obj.x), V8 caches: the expected hidden class + the property offset. On next access: check if same hidden class → if yes, use cached offset directly — no lookup needed!

IC states evolve as V8 sees more shapes:

Uninitialized → Monomorphic (1 class) → Polymorphic (2-4 classes) → Megamorphic (5+ classes)

Monomorphic: fastest — one cached offset, direct memory access. Polymorphic: slower — check against up to 4 classes in a list. Megamorphic: slowest — give up caching, use hash map lookup every time.

// Monomorphic access site — FAST
function getX(point) { return point.x; }
const pts = [
  { x: 1, y: 2 }, // All same hidden class
  { x: 3, y: 4 },
  { x: 5, y: 6 },
];
pts.forEach(p => getX(p)); // IC: Monomorphic — one class seen

// Polymorphic — slower
function getX2(obj) { return obj.x; }
getX2({ x: 1, y: 2 });          // class A
getX2({ x: 1, z: 3 });          // class B — different!
getX2({ x: 1, w: 4 });          // class C — IC becomes polymorphic

// Megamorphic — IC gives up
function processItem(item) { return item.value; }
// Called with 10 different object shapes:
// { value: 1, a: 1 }, { value: 2, b: 2 }, { value: 3, c: 3 }...
// IC becomes megamorphic — every access is a slow hash lookup
The terms monomorphic, polymorphic, megamorphic describe call sites, not objects.
A single function called with 5 different shaped objects has a megamorphic call site. This is why utility functions that accept "any object with .value" can be slower than type-specific functions.
03Property Access Patterns & Optimization

Property order determines hidden class — same properties in different order = different class. Add all properties in constructor/factory — don't add later.

Don't delete properties — use null or undefined instead of delete. Prototype properties are fast — shared across instances via prototype chain, one hidden class for all.

Own property vs prototype property performance: similar — V8 ICs handle both efficiently. Dense arrays (no holes) vs sparse arrays (with gaps): dense is much faster (PackedSMI, PackedDouble, etc.).

// ✅ Good: all properties in constructor, consistent order
function createPoint(x, y) {
  return { x, y }; // always same hidden class
}
const pts = Array.from({ length: 1000 }, (_, i) => createPoint(i, i));

// ❌ Bad: inconsistent property addition
function createUser(data) {
  const u = {};
  if (data.name) u.name = data.name;   // sometimes
  if (data.email) u.email = data.email; // sometimes
  return u;
  // Creates MANY different hidden classes depending on which fields exist
}

// ✅ Good: always all properties (use null for missing)
function createUser2(data) {
  return {
    name:  data.name  ?? null,
    email: data.email ?? null,
    // Always same shape — always null or string, never absent
  };
}

// Dense vs sparse array
const dense = [1, 2, 3, 4, 5];       // PACKED_SMI — fastest
const sparse = [1, , , 4, 5];         // HOLEY — slower
const withFn = [1, () => {}, 3];      // PACKED_ELEMENTS — mixed types, slower
V8's array element kinds: PACKED_SMI_ELEMENTS (only small integers, fastest), PACKED_DOUBLE_ELEMENTS (only floats), PACKED_ELEMENTS (any values), then the HOLEY versions of each (with gaps). Once an array gets demoted (e.g., adds a non-integer), it can never go back to a faster kind. Avoid mixed-type arrays in hot paths.
04Deoptimization Triggers & How to Debug

Deoptimization (deopt): TurboFan's compiled code becomes invalid, falls back to bytecode interpreter.

Common deopt triggers: type change in hot function, hidden class change, accessing uninitialized property.

Soft deopt: fall back to bytecode, re-profile, possibly re-optimize later with looser assumptions. Hard deopt (bailout): something fundamentally unoptimizable — stays as bytecode permanently.

How to see deopts: node --trace-deopt --trace-opt yourfile.js. Chrome DevTools Performance tab: look for yellow "deoptimize" events in call tree.

// Deopt example — type change
function multiply(a, b) { return a * b; }

// 1000 calls with integers — TurboFan optimizes for integers
for (let i = 0; i < 1000; i++) { multiply(i, i); }

// DEOPT: suddenly pass a float
multiply(1.5, 2.5); // TurboFan's assumption broken → deopt!
// V8 recompiles with float support

// Check deopt in Node.js:
// node --trace-deopt script.js
// Output: [deoptimize] multiply reason: Insufficient type feedback

// Avoid deopts:
// ❌ Inconsistent types
function process(x) { return x * 2; }
process(1);     // integer
process(1.5);   // float — deopt
process('hi');  // string — deopt again

// ✅ Consistent types
function processInt(x)    { return x * 2; }   // always int
function processFloat(x)  { return x * 2.0; } // always float
Key insight: V8 doesn't deoptimize because your code is "wrong" — it deoptimizes because its assumptions were violated. TurboFan compiles based on observed types. If you change the types, the compiled code is invalid. Consistent types = stable optimization = fast code.
05Writing V8-Friendly Code: Practical Rules

Rule 1: Initialize all object properties in constructor with correct types. Rule 2: Never delete properties — set to null/undefined instead. Rule 3: Keep function arguments consistent in type.

Rule 4: Avoid mixing types in arrays — use typed arrays for numbers. Rule 5: Prefer prototype methods over per-instance closures for methods. Rule 6: Use monomorphic call sites — don't pass wildly different objects to same function.

Rule 7: For hot numeric loops, use integer arithmetic where possible.

// ❌ Anti-patterns
class BadPoint {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  getDistance() { // Per-instance closure — new function per instance!
    return Math.sqrt(this.x ** 2 + this.y ** 2);
  }
}

// ✅ Prototype method — shared across all instances (one hidden class)
class GoodPoint {
  constructor(x, y) {
    this.x = x; // always number
    this.y = y; // always number
    // No conditional properties!
  }
}
GoodPoint.prototype.getDistance = function() {
  return Math.sqrt(this.x ** 2 + this.y ** 2);
};

// Typed arrays for numeric hot paths
const normalArray = [1, 2, 3, 4, 5];     // any element type
const typedArray = new Float64Array(5);  // V8 guarantees float64
typedArray[0] = 1; typedArray[1] = 2;   // no hidden class changes possible!

// Integer arithmetic — V8 SMI optimization
function sumSMI(arr) {
  let total = 0; // SMI (small integer) — fastest!
  for (let i = 0; i < arr.length; i++) {
    total += arr[i]; // stays SMI if arr has only integers
  }
  return total;
}
When to optimize: These V8 optimization rules matter most in hot paths — code called thousands of times: render loops, data processing pipelines, event handlers. For code called once or rarely, readability beats micro-optimization. Profile first with node --prof or Chrome Performance tab, then optimize the actual hotspot.

Engine Room Cleared — Key Points:

  • ✅ Hidden classes (Shapes): V8's internal object structure — two objects with same properties in same order share a hidden class, enabling fast property access
  • ✅ Property order matters — adding properties in different order creates different hidden classes, preventing sharing
  • ✅ Inline Caches remember the hidden class + property offset at each access site — monomorphic is fastest, megamorphic gives up
  • ✅ Deoptimization: TurboFan's type assumption violated → compiled code discarded → back to bytecode → re-profile
  • ✅ Dense arrays (no holes, consistent type) are much faster than sparse/mixed arrays — V8 has 6 element kinds
  • ✅ Write V8-friendly code: initialize all properties upfront, never delete, keep types consistent, use prototype methods
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase