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:

  1. A pointer to its current hidden class/shape.
  2. 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:

  1. Initial Shape: Creating an empty object (const a = {}) assigns it an initial empty hidden class (e.g., Class0).
  2. Property Addition: Adding a property (a.x = 1) causes the engine to transition the object from Class0 to a new hidden class (Class1), which defines property x at offset 0.
  3. Subsequent Additions: Adding another property (a.y = 2) triggers another transition from Class1 to Class2, which records property y at offset 1.

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: