V8 Hot code path — Part 2 : Hidden Class
It simply means to not add properties after construction of object because late addition creates a new hidden class transition in the V8 to…
V8 Hot code path — Part 2 : Hidden Class
It simply means to not add properties after construction of object because late addition creates a new hidden class transition in the V8 to update the shape of the object.
The practical rule is simple: always declare all properties inside the constructor, always in the same order. That keeps every instance on the same single path through the transition tree, all inline caches stay monomorphic, and V8 never has to recompile or bail out.

You can run the below script to test out the hypothesis mentioned above
'use strict';
const ITERATIONS = 500_000;
const BATCH_SIZE = 8; // objects allocated and read per iteration
// ─── SLOW: forked transition tree ─────────────────────────────────────────────
// Constructor sets only .name (H0 → H1).
// Caller alternates the order it adds .role and .age, forking the tree:
//
// even: H1 --role--> H2a --age --> H3a
// odd: H1 --age --> H2b --role--> H3b
//
// The read loop at the bottom of the batch touches objects of both shapes.
// Every .age and .role read site becomes polymorphic → IC misses on half
// the reads → deopt storm inside the optimized code.
function User_slow(name) {
this.name = name; // H0 → H1
}
function benchSlow(label) {
let sink = 0;
const batch = new Array(BATCH_SIZE);
for (let i = 0; i < 10_000; i++) {
for (let b = 0; b < BATCH_SIZE; b++) {
const u = new User_slow(i % 2 ? 'alice' : 'bob');
if ((i + b) % 2 === 0) {
u.role = (i + b) % 3 ? 'admin' : 'user';
u.age = ((i + b) % 50) + 18;
} else {
u.age = ((i + b) % 50) + 18;
u.role = (i + b) % 3 ? 'admin' : 'user';
}
batch[b] = u;
}
for (let b = 0; b < BATCH_SIZE; b++) {
sink += batch[b].age + batch[b].name.length + batch[b].role.length;
}
}
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
for (let b = 0; b < BATCH_SIZE; b++) {
const u = new User_slow(i % 2 ? 'alice' : 'bob');
if ((i + b) % 2 === 0) {
u.role = (i + b) % 3 ? 'admin' : 'user'; // H1 → H2a
u.age = ((i + b) % 50) + 18; // H2a → H3a
} else {
u.age = ((i + b) % 50) + 18; // H1 → H2b ← different order
u.role = (i + b) % 3 ? 'admin' : 'user'; // H2b → H3b
}
batch[b] = u;
}
// Read loop — hits polymorphic IC sites on every other object.
for (let b = 0; b < BATCH_SIZE; b++) {
sink += batch[b].age + batch[b].name.length + batch[b].role.length;
}
}
const ms = (performance.now() - start).toFixed(2);
console.log(` ${label.padEnd(44)} ${ms.padStart(8)}ms`);
if (sink === 0) throw new Error('dead-code guard');
}
// ─── FAST: all properties in the constructor, same order every time ───────────
// The constructor always walks H0→H1→H2→H3 in the same sequence.
// Every instance in the batch shares the same hidden class.
// All IC sites stay monomorphic — zero misses, zero deopts.
function User_fast(name, role, age) {
this.name = name; // H0 → H1
this.role = role; // H1 → H2 ← same order, always
this.age = age; // H2 → H3
}
function benchFast(label) {
let sink = 0;
const batch = new Array(BATCH_SIZE);
for (let i = 0; i < 10_000; i++) {
for (let b = 0; b < BATCH_SIZE; b++) {
batch[b] = new User_fast(
i % 2 ? 'alice' : 'bob',
(i + b) % 3 ? 'admin' : 'user',
((i + b) % 50) + 18,
);
}
for (let b = 0; b < BATCH_SIZE; b++) {
sink += batch[b].age + batch[b].name.length + batch[b].role.length;
}
}
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
for (let b = 0; b < BATCH_SIZE; b++) {
batch[b] = new User_fast(
i % 2 ? 'alice' : 'bob',
(i + b) % 3 ? 'admin' : 'user',
((i + b) % 50) + 18,
);
}
// Read loop — all objects share the same hidden class → monomorphic ICs.
for (let b = 0; b < BATCH_SIZE; b++) {
sink += batch[b].age + batch[b].name.length + batch[b].role.length;
}
}
const ms = (performance.now() - start).toFixed(2);
console.log(` ${label.padEnd(44)} ${ms.padStart(8)}ms`);
if (sink === 0) throw new Error('dead-code guard');
}
benchSlow('SLOW — forked hidden class tree ');
benchFast('FAST — stable hidden class, one path ');
console.log('\n Expected in trace output:');
console.log(' SLOW → [bailout] inside benchSlow due to polymorphic IC on .age / .role');
console.log(' benchSlow recompiled multiple times; User_slow optimized once');
console.log(' FAST → single [completed optimizing] each for benchFast + User_fast');
console.log(' no bailouts, no recompilations\n');Post running it using the command provided below
node --trace-opt --trace-deopt s2_hidden_class.js 2>&1
you will the output like this

and as u can see that hidden class takes 35ms compared to stable path taking 27ms
메타데이터
- post_id
- ae68015dfcda
- slug
- v8-hot-code-path-part-2-hidden-class-ae68015dfcda
- url
- https://medium.com/@chadhamoksh/v8-hot-code-path-part-2-hidden-class-ae68015dfcda
- canonical_url
- https://medium.com/@chadhamoksh/v8-hot-code-path-part-2-hidden-class-ae68015dfcda
- author_url
- https://medium.com/@chadhamoksh
- status
- ok
- fetched_at
- 2026-06-17 08:20:12