JavaScript Engine Hidden Classes and Shapes Explained
This article explores how modern JavaScript engines use hidden classes—also known as shapes or structures—to optimize dynamic object property access. You will learn how these internal representations track object layouts, how transition trees work behind the scenes, how they enable Inline Caching for near-instantaneous property lookups, and the specific programming habits you can adopt to write high-performance JavaScript.
The Problem with Dynamic Objects
JavaScript is a dynamically typed language, meaning objects can have properties added, modified, or removed at runtime. In languages like C++ or Java, object structures (layouts) are determined at compile time and stored at fixed memory offsets, allowing property access via a single direct memory read.
Without internal optimizations, JavaScript engines would have to treat objects like hash maps, requiring an expensive hash lookup every time a property is accessed or modified. To achieve near-native execution speeds, engines like V8 (Chrome/Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) map dynamic objects to internal structures called hidden classes (V8), shapes (SpiderMonkey), or structures (JavaScriptCore).
How Hidden Classes and Shapes Work
A hidden class or shape is an immutable metadata descriptor created by the engine. Instead of storing property names and offsets repeatedly inside every individual object instance, the object itself stores:
- A pointer to its current hidden class/shape.
- An array of actual property values (stored as a flat list).
The hidden class contains a dictionary of property names along with their corresponding index offsets in the values array. When multiple objects share the same properties added in the identical order, they point to the exact same hidden class, dramatically reducing memory usage and lookup overhead.
Transitions and Transition Trees
When an object is created and modified, the engine tracks its evolution through a transition tree:
- Initial Shape: Creating an empty object
(
const a = {}) assigns it an initial empty hidden class (e.g.,Class0). - Property Addition: Adding a property
(
a.x = 1) causes the engine to transition the object fromClass0to a new hidden class (Class1), which defines propertyxat offset0. - Subsequent Additions: Adding another property
(
a.y = 2) triggers another transition fromClass1toClass2, which records propertyyat offset1.
If a second object b is instantiated
(const b = {}) and receives property x
followed by y, it traverses the exact same transition path
(Class0 -> Class1 ->
Class2). Both objects now share Class2.
However, if a third object c receives property
y first and then x, the engine must branch and
create a separate transition path (Class0 ->
Class3 -> Class4). Even though
a and c have the same final properties, they
end up with different hidden classes because the properties were
initialized in a different order.
Enabling Inline Caching (IC)
The primary reason engines maintain hidden classes is to enable
Inline Caching (IC). When a function accesses an object property
repeatedly (e.g., return obj.x), the engine records the
object’s hidden class and the property offset.
On subsequent calls: - If the passed object shares the cached hidden class (monomorphic access), the engine skips the property lookup entirely and reads directly from the known memory offset. - If the function receives objects with two to four different shapes (polymorphic access), the engine uses a small lookup table. - If the function encounters more shapes (megamorphic access), it de-optimizes and falls back to a slow, generic property lookup.
Best Practices for JavaScript Performance
To help the JavaScript engine optimize property access and maintain monomorphic call sites, follow these rules:
- Initialize properties in constructors: Define all object properties upfront inside constructor functions or class definitions rather than adding them later.
- Maintain a consistent property order: Always assign properties in the exact same sequence across object instances.
- Avoid deleting properties: Using the
deleteoperator changes an object’s shape unpredictably and often forces the engine to switch that object into a slow hash-map dictionary mode. Set unused properties tonullorundefinedinstead.