Hidden Classes, Inline Caches & V8 Optimization
V8 speed deta hai — par condition hai. Shape change mat karo bhai!
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
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
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.
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
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
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;
}
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
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login