Understanding hidden classes in V8: Why hash tables aren’t enough?
Oct 25, 2025While debugging performance issues in our collection runner, I came across this article from v8 explaining how hidden classes are used to optimize performance. The collection runner’s performance issue was not directly related to the stuff I am going to explain in this article but it helped me understand the internals of v8.
When you write JavaScript code, you're working with a dynamic language where objects can change shape at runtime. You can add properties, delete them, or modify them whenever you want. This flexibility is one of JavaScript's greatest strengths, but it comes with a significant performance cost. To understand why modern JavaScript engines like V8 are so fast despite this dynamic nature, we need to understand hidden classes (also called Maps or Shapes in different engines).
The Problem: Why Hash Tables Are Slow
How Static Languages Handle Object Properties
In statically-typed languages like C++ or Java, the compiler knows exactly what properties an object will have at compile time. Consider this C++ class:
class Point {
public:
int x;
int y;
};
The compiler can determine that x is at offset 0 and y is at offset 4 (assuming 4-byte integers). Accessing point.x becomes a simple memory operation: take the object's base address and add the offset. This is incredibly fast—often just a single CPU instruction.
The Dynamic Lookup Problem in JavaScript
JavaScript, being a dynamic language, doesn't have this luxury. Consider this code:
const point = {};
point.x = 10;
point.y = 20;
When the engine encounters point.x, it has no idea where x is stored. The naive approach would be to use a hash table (dictionary):
- Hash the property name "x" to get a hash code
- Look up the hash code in the hash table
- Handle potential hash collisions
- Return the value
Why is this slow?
- Hashing overhead: Computing a hash for every property access is expensive
- Collision resolution: When two properties hash to the same value, the engine must resolve the collision by comparing keys
- Cache inefficiency: Hash tables scatter data in memory, leading to poor CPU cache utilization
- No optimization potential: Each property access is independent, making it difficult for the JIT compiler to optimize
In practice, hash table lookups for property access can be 10-100x slower than direct offset access, especially when considering cache effects and collision handling.
Let's see a real example of the performance difference:
// Heap/dictionary mode (slow)
function createSlowPoint(x, y) {
const point = {};
point.x = x;
point.y = y;
delete point.x; // Triggers dictionary mode!
point.x = x;
return point;
}
// Hidden class mode (fast)
function createFastPoint(x, y) {
return { x, y }; // All properties defined at once
}
// Performance test
const iterations = 1_000_000;
console.time('Slow (dictionary mode)');
for (let i = 0; i < iterations; i++) {
const p = createSlowPoint(i, i * 2);
const sum = p.x + p.y;
}
console.timeEnd('Slow (dictionary mode)');
console.time('Fast (hidden class)');
for (let i = 0; i < iterations; i++) {
const p = createFastPoint(i, i * 2);
const sum = p.x + p.y;
}
console.timeEnd('Fast (hidden class)');
On my machine, dictionary mode million iterations are 100x slower.
Results on my machine:
- Fast (hidden class): ~2ms
- Slow (dictionary mode): ~125ms
Understanding the performance difference
The magic is because of hidden classes.
The difference is dramatic: hidden classes transform dynamic property lookups into static, offset-based memory access—essentially making JavaScript objects perform like C structs.
While hash tables are O(1) on average, the constant factor is 10-100x larger than direct offset access. For property access in a loop running millions of times, this difference is massive.
Dictionary mode uses hash table while fast property uses hidden classes for optimization.
Hidden classes
Hidden classes are V8's way of bringing the performance of static languages to JavaScript. Instead of treating objects as arbitrary bags of properties, V8 tracks the shape or structure of objects and creates an internal class-like structure for each unique shape.
1. Initial state (C0):
When new Point() is called, V8 creates an empty hidden class
- [hidden_class_ptr: C0]
2. Adding 'x' property (C0 → C1):
When this.x = x executes:
- V8 creates hidden class C1
- C0: "if property 'x' added → go to C1"
- [hidden_class_ptr: C1][x: 1]
3. Adding 'y' property (C1 → C2):
When this.y = y executes:
- V8 creates hidden class C2
- C1: "if property 'y' added → go to C2"
- [hidden_class_ptr: C2][x: 1][y: 2]
4. Reusing hidden classes:
When p2 = new Point(3, 4) is created:
- Follows same path: C0 → C1 → C2
- NO new hidden classes created!
- Both p1 and p2 point to the same C2
- Memory: [hidden_class_ptr: C2][x: 3][y: 4]
// Objects with the same structure share the same hidden class.
The transitions are stored through directed acyclic graphs. Thats why property order matters.
// These create DIFFERENT hidden classes!
const obj1 = {};
obj1.x = 1; // C0 → C1
obj1.y = 2; // C1 → C2
const obj2 = {};
obj2.y = 2; // C0 → C5 (different branch!)
obj2.x = 1; // C5 → C6
// obj1 has hidden class C2: { x: offset 0, y: offset 1 }
// obj2 has hidden class C6: { y: offset 0, x: offset 1 }
// These are DIFFERENT hidden classes with different offsets!
Inline caches
Hidden classes enable another critical optimization: inline caching (IC). This is where the real performance gain comes from. When V8 executes a function, it remembers which hidden classes it encountered like when creating p2, the same hidden classes are reused.
When a function accesses obj.x multiple times with the same hidden class, V8 caches the offset location. On subsequent calls, V8 can skip the property lookup entirely and jump directly to the cached memory offset.
Read more about monomorphic, polymorphic and megamorphic states.
Why deletion triggers dictionary mode?
Hidden classes rely on fixed, contiguous memory offsets:
const obj = {};
obj.x = 1; // Hidden class C1: x at offset 0
obj.y = 2; // Hidden class C2: x at offset 0, y at offset 1
obj.z = 3; // Hidden class C3: x at offset 0, y at offset 1, z at offset 2
// Memory layout:
// [C3_ptr][x: 1][y: 2][z: 3]
// ↑ offset 0
// ↑ offset 1
// ↑ offset 2
When any property is deleted:
delete obj.y;
// Problem: Memory now has a HOLE
// [C3_ptr][x: 1][HOLE][z: 3]
// ↑ offset 0
// ↑ offset 2 (but should be offset 1?)
The transition chain/graph can no longer be maintained. The engine could create another hidden class to fill this hole but $2^n$ combinations would be present. Instead of having so many number of hidden classes, v8 takes a simple route to manage it using hash table when any property is deleted.
There are other conditions also which trigger dictionary mode like when megamorphic objects are present (objects with too many hidden classes). IC cannot optimize so it falls back to hash table.
Conclusion
Hidden classes are V8's brilliant solution to JavaScript's dynamic nature. By tracking object shapes and creating internal structures that resemble static class definitions.
Understanding hidden classes isn't just theoretical knowledge—it has practical implications:
- Avoid property deletion - It triggers slow dictionary mode
- Keep property order consistent - Different orders = different hidden classes
- Initialize all properties upfront - Even null/undefined values
- Write monomorphic code - Process objects with the same shape