JavaScript Objects, Hidden Classes, and Inline Caches
JavaScript engines often represent objects with hidden classes—internal descriptions of an object’s property layout—and speed repeated property access with inline caches, small pieces of cached lookup logic at the access site. When an object’s layout changes, the engine can invalidate those assumptions, discard optimized machine code, and return to a more general path. This process is called deoptimization.
The problem: dynamic objects are expensive to look up
JavaScript lets objects change at runtime:
const user = {};
user.name = "Ada";
user.active = true;
A property access such as user.name cannot generally be compiled as “read a fixed field at a fixed offset” without first knowing what user really is. The value might be an ordinary object, an object with a getter, a proxy, or an object whose property was added or removed. The property might also be inherited through the prototype chain—the linked sequence of objects JavaScript searches when a property is not found directly on the object.
A fully general lookup handles all of those possibilities. It may inspect the object, search its properties, walk its prototypes, and account for accessors. Doing that on every iteration of a hot loop would be costly.
Hidden classes and inline caches let the engine make the common case cheap while preserving JavaScript’s dynamic behavior.
A hidden class describes a layout
A hidden class, also called a shape or structure in different engines, is metadata shared by objects that have the same relevant property layout. It is not the object’s JavaScript-visible class, and it does not contain the property values themselves.
For example, these objects usually acquire the same layout:
function Point(x, y) {
this.x = x;
this.y = y;
}
const first = new Point(10, 20);
const second = new Point(30, 40);
The engine can describe their shared layout roughly as:
x -> slot 0
y -> slot 1
The actual values 10, 20, 30, and 40 live in the individual objects. The shared shape says where to find them.
Engines normally create shapes through transitions. An initially empty object has one shape. Adding x moves it to a shape describing an object with x; adding y moves it to another shape describing x followed by y. Many objects created in the same order can therefore share the resulting shape and storage layout.
Property order can matter:
const a = { x: 1, y: 2 };
const b = { y: 2, x: 1 };
These objects have the same visible properties, but they may end up with different shapes because their construction histories differ. The engine can still execute both correctly; it simply has fewer opportunities to reuse one layout description.
Adding or deleting properties, changing certain object characteristics, or accumulating many irregular properties can make an object harder to represent with a compact layout. An engine may move such an object to a less specialized dictionary-like representation, where properties are managed more like a general hash table.
An inline cache remembers what worked
An inline cache (IC) is cached information attached to a particular operation in the program, such as one obj.x expression. After the engine performs that lookup once, it can remember the object shape it saw and the corresponding action.
Conceptually, code for point.x can become:
if point.shape == ShapeWithXAtSlot0:
return point.slot[0]
else:
perform the general property lookup
The shape check is a guard: a quick test that must succeed before the shortcut is safe. If it succeeds, the engine reads the value directly instead of repeating the complete lookup.
This is especially useful in loops and frequently called functions. The source code still says point.x, but the generated code may reduce repeated accesses to a shape comparison and a fixed-offset load.
An access site that has only observed one shape is called monomorphic—“one form.” If the same expression receives a small number of different shapes, the engine can use a polymorphic inline cache, which checks several known shapes:
if shape == PointShape: read slot 0
else if shape == VectorShape: read slot 1
else: use the general lookup
After enough different shapes, the site may become megamorphic, meaning it has seen so many forms that a long list of specialized checks is no longer worthwhile. The engine generally uses a more generic lookup in that case. The exact thresholds and implementation differ among V8, SpiderMonkey, JavaScriptCore, and other engines, but the strategy is broadly similar.
Where deoptimization fits
Modern JavaScript engines first interpret code or compile it with modest assumptions, then identify code that runs frequently and compile it more aggressively. That optimized code may assume that a particular property access will keep seeing a particular shape.
Consider a function called repeatedly with similarly constructed objects:
function total(point) {
return point.x + point.y;
}
const point = { x: 3, y: 4 };
for (let i = 0; i < 1_000_000; i++) {
total(point);
}
point.z = 5;
Adding z usually creates a new shape for point. Existing optimized code that assumed the old shape may encounter a failed guard. The engine then deoptimizes: it exits the optimized machine code, reconstructs the program’s state in a form understood by a less specialized execution path, and continues correctly. The inline cache may be updated to recognize the new shape, or the code may be recompiled with broader assumptions later.
Deoptimization is not an error and does not mean the program is incorrect. It is a recovery mechanism for optimistic compilation. However, repeated shape changes or a function that receives many unrelated shapes can prevent stable optimization and increase runtime overhead.
What this means in practice
This mechanism is one reason consistent object construction often helps performance:
- Initialize the same properties for similar objects.
- Initialize them in the same order.
- Avoid repeatedly adding and deleting properties on hot objects.
- Keep hot functions from receiving a large mixture of unrelated object layouts.
These are performance guidelines, not correctness rules. JavaScript remains dynamic, and engines handle irregular objects correctly. A shape transition may be cheap, and a deoptimization may never matter in an application dominated by network or database time.
When a profiler or runtime log mentions hidden classes, shapes, inline caches, megamorphic accesses, or deoptimization, it is describing this trade-off: the engine tried to turn a dynamic property lookup into a predictable fixed-layout operation, then widened or abandoned that shortcut when the objects no longer matched its assumptions.