Hidden Classes (Shapes), Illustrated
You can prove to yourself that JavaScript objects are secretly typed, even though the language swears they are not.
const a = { x: 1, y: 2 };
const b = { y: 2, x: 1 };
To your code, a and b are interchangeable. Same keys, same values, a.x === b.x, deep-equal by any check you write. To the engine running them, they are two different kinds of object with two different internal layouts. A function that reads .x from a mix of both is slower than the same function reading .x off a million objects that were all built like a. Nothing about the reads changed. The objects did.
The thing that makes a and b different kinds is a hidden class. It is the most useful internal to understand if you want to know why some object-heavy code flies and some crawls, so this lesson takes it slowly and draws a lot of pictures.
The cost the engine is dodging
Picture the naive way to store an object: a hash map from property names to values. Every point.x would then be a hash-table lookup. Hash the string "x", find the bucket, deal with collisions, read the value out. Not expensive once. Ruinous a billion times inside an animation loop.
There is a worse problem underneath the speed one. In a dynamically typed language, the code compiling point.x has no idea at compile time where x lives, or whether this particular point even has an x. Any object could have any layout. So the only safe thing to emit for point.x is the full dynamic lookup, every single time.
The way out is to notice that real programs do not actually make wildly unique objects. You write one shape of point and then create ten million of them, all with the same two fields in the same order. The engine bets everything on that regularity.
The hidden class
Here is the trade V8 makes. Instead of storing property names on each object, it stores the values in a plain array of numbered slots, and it moves the layout into a separate, shared object. Every object gets a pointer to a hidden class: a description of the object’s structure that lists which properties it has and, for each one, the fixed slot where the value lives.
With the shape in hand, point.x no longer means “search this object for a property called x”. It means “check that this object’s shape is the one we expect, then read slot 0”. A pointer comparison and an array index. That check-then-read pair is exactly what an inline cache remembers so it does not have to re-derive it on the next spin of the loop.
Same structure, same shape
The reason a shape is worth having is that it gets shared. Two objects with the same properties in the same order point at the same shape object, not two equal copies of it. The same one, by pointer. A million points built the same way share a single shape between them.
That sharing is the whole payoff. The shape check is cheap because it is a pointer comparison, “is this object’s shape the exact shape my fast code was compiled for?”, and it is worth caching because the answer is “yes” nearly every time. If every object had its own private layout, there would be nothing to compare against and nothing to cache. Shapes only earn their keep when many objects agree on one.
Building a shape, one property at a time
Where do shapes come from? The engine grows them lazily as your objects grow. Start from an empty object. It gets the empty shape. Add a property and the object transitions to a new shape that includes it. Add another and it transitions again. Each step forward is a new shape describing one more property.
The transitions themselves are cached, which is the clever part. When the engine is about to add y to an object that currently has the “has x” shape, it first asks the shape a question: do you already have a transition for adding y? If yes, it follows that edge and reuses the shape on the other end. The first object of a kind blazes the trail and mints the shapes. Every object after it walks the same trail for free and lands on the exact same shapes.
The shapes form a tree. The empty shape is the root, each edge is a property name, and following the edges from the root spells out the exact order an object’s properties were added. This is why the folklore about property order is not folklore. The order is the path, and the path is the shape.
Order is identity
Now a and b from the opening can part ways in public. Because a shape is defined by which properties in which order, adding the same properties in a different order walks a different branch of the tree and ends on a different shape.
To your program, “has x, y” and “has y, x” store identical data and behave identically. To the engine they are two separate shapes, as unrelated as “has x” and “has colour”. Any code that reads .x off both objects now has to handle two shapes where it could have handled one.
Why divergent shapes read as slow code
A spot in your code that reads a property, say the p.x inside some function, keeps a small note about the shapes it has seen there. If it has only ever seen one shape, the engine specialises hard: one shape check, then the direct slot read. If it has seen a few, it emits a short chain of checks. If it has seen too many, it gives up on specialising and falls back to a fully generic lookup that works for anything and is fast for nothing.
Those three states have names, monomorphic, polymorphic, and megamorphic, and the full machinery that records them is its own lesson on type feedback and speculation. The part that matters here is the input to that machinery: inconsistent object construction is one of the main ways a property read drifts from seeing one shape to seeing many.
So the most-repeated performance tip about objects has a real mechanism behind it: give every object of a kind the same properties in the same order, ideally assigned once up front, and property reads across them stay monomorphic and take the direct-offset path.
// shape-stable: every Point walks the same transition path,
// so every Point ends up sharing one shape.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
Compare that to a factory that grows its objects unevenly:
// shape-diverging: the conditional z means some objects go
// x -> z -> y and others go x -> y. Two shapes for one "kind".
function makePoint(x, y, depth) {
const p = {};
p.x = x;
if (depth !== undefined) p.z = depth; // sometimes present
p.y = y; // lands in different places
return p;
}
Every Point from the class shares one shape. The objects from makePoint split into at least two shapes depending on whether depth was passed, and any function reading them sees both. You did not intend to make two types. The transition tree made them for you.
See the shapes split
Build two objects below by adding properties in whatever order you like. Watch each object’s shape id, and watch the shared transition tree grow. Add the same properties in the same order to both and they land on one shape. Reset one and add them in a different order, and the tree branches, exactly like the diagram above. The shape ids here are simulated to show the structure, not real V8 numbers.
What actually breaks shape sharing
Four things pull objects of one apparent kind onto different shapes:
- Different insertion orders. Two branches of the tree, as above.
- Conditional properties. A field that only some instances get. The ones with it and the ones without it are different shapes.
- Properties added later, in different places. If some instances gain a field in one code path and others gain it somewhere else, they grow into different shapes.
- Deleting properties. This one is special enough to get its own section.
delete and dictionary mode
delete obj.prop is the sharp edge. Shapes only ever grow forward along the tree, so there is no clean transition for taking a property away. Faced with a delete, V8 often stops trying to describe the object with a shared shape at all and moves it into dictionary mode, also called slow mode or normalized properties.
In dictionary mode the object carries its own self-contained table of name to value. It is back to the naive model this whole scheme was built to avoid: flexible, and slow, because the optimiser cannot bet on a layout that lives inside each object and can change at any time.
Be honest about the caveats, because this area is a magnet for superstition. Not every delete demotes an object, V8 has heuristics and can sometimes handle removing the most recently added property more gracefully, and the exact triggers shift between versions. The safe reading is directional, not a magic rule: delete on an object you touch in a hot loop is a reliable way to make it slow, and if you find yourself deleting keys constantly, that is a signal about the data structure you picked. Setting the property to undefined keeps the shape intact if you only need to clear a value; removing the key is what risks the demotion.
When your object is really a Map, use Map
Step back from the mechanics for the practical call. If you are using an object as a genuine dictionary, arbitrary keys that come and go at runtime, keyed by data rather than by a fixed set of field names, you are fighting the shape system on every insert and delete. The honest fix is not to contort the object. It is to reach for Map, which was built for exactly that job: keys in, keys out, no transition tree, no dictionary-mode cliff. Objects are for records with a roughly known set of fields. Map is for dictionaries. Picking the right one makes the shape question disappear rather than something you have to manage.
The honest calibration
Now the part that internals writing usually skips, because it is less fun than the mechanism. This matters in a narrow set of cases and is irrelevant almost everywhere else.
It is worth caring about when you create the same kind of object in large numbers and read its properties in a hot loop or a hot function. Points in a physics step, nodes in a parser, rows in a data grid, particles, vectors. There, keeping one shape is the difference between a direct load and a generic lookup, multiplied by millions.
It is not worth caring about for a config object you build once and read a handful of times, an options bag, a one-off result, or the overwhelming majority of application code. The shape of your settings object is not your bottleneck, and reordering its keys to “help the engine” is a waste of a code review. You will make the code stranger and measure no difference.
Same idea in every engine
Hidden classes are not a V8 invention. Every serious engine faces the same problem, how to make property access on a dynamically typed object cheap, and every one solved it the same way: describe an object’s structure once, share that description across objects that agree on it, and cache the check.
What this means for your code
- Build a kind of object the same way every time. Same fields, same order, ideally all assigned in one constructor or factory, so instances share a shape and reads across them stay on the fast path.
- Prefer up-front fields over bolted-on ones. Assigning all properties at construction beats adding them later in scattered code paths, which mints extra shapes.
- Avoid
deleteon hot objects. Clear a value withundefinedif that is all you need. Removing keys risks dropping the object into dictionary mode. - Use
Mapfor real dictionaries. If keys are data and churn at runtime, aMapsidesteps the shape system entirely instead of thrashing it. - Do not cargo-cult it. This is a tool for a profiled hotspot, not a style to impose on every object. Consistent construction is free; contorting cold code is not worth it.
Summary
- V8 stores object values in numbered slots and puts the layout in a shared hidden class (a shape) that maps each property name to a fixed slot. Property access becomes a shape check plus a direct slot read, not a name lookup. That check-and-read is what an inline cache remembers.
- Objects with the same properties in the same order share one shape, by pointer. That sharing is what makes the check cheap and cacheable.
- Shapes are built by transitions: each added property moves an object to a new shape, and the transitions form a tree rooted at the empty shape. Two objects that add the same properties in the same order walk the same edges to the same shape.
- Order is identity. Same keys in a different order means a different shape. Divergent shapes push a property read from monomorphic toward megamorphic, trading the direct load for a generic lookup.
deletecan drop an object into slow dictionary mode, where inline caches no longer apply. For objects used as real dictionaries, preferMap.- Every engine implements the same concept under a different name (Maps, Shapes, Structures). Tier names and exact triggers are engine-specific and change over time.
What this means for your code: for the objects you make in bulk and read in hot paths, build them consistently so they share a shape, and the fast direct-offset access stays available. For everything else, write the clearest code and let the engine worry about it.