Concepts — Object Shapes, Hidden Classes, Inline Caches & JIT
Phase 1 · Platform substrate · Specification area §1 (object shapes, hidden classes, inline caches, JIT compilation). Parallel-safe with fe-04–fe-07.
This module's thesis is unusual: its main deliverable is knowing when to ignore it. The effects are real and reproducible. In frontend code they are almost always the wrong thing to work on, and being able to say so with numbers is the Principal-level skill here.
1. What is it
V8 does not store JavaScript objects as hash maps. It infers structure and specialises:
- Hidden classes (Maps/Shapes) describe an object's layout — which properties exist, in what order, at which offsets. Objects built identically share one hidden class.
- Inline caches (ICs) memoise, at each property-access site, which shapes have been seen there and where the property lives. One shape means a direct offset load.
- Element kinds specialise array storage by content: small integers, doubles, or general elements; packed or holey. Transitions are one-way.
- Tiered JIT — an interpreter (Ignition) plus optimising compilers (Sparkplug/Maglev/TurboFan) that speculate on observed types and deoptimise when the speculation breaks.
All of it is speculation on consistency. Consistent code runs fast; inconsistent code falls back.
2. Why it matters
Three legitimate reasons, and one illegitimate one worth naming.
Legitimate — you own a hot data path. Virtualized tables, canvas/WebGL render loops, parsers, diffing algorithms, worker-side transforms over 10⁵–10⁷ items. Here per-element costs multiply by enough to matter (fe-30).
Legitimate — you are choosing a data representation. Array-of-objects vs object-of-arrays,
Map vs plain object as a dictionary, whether to delete. These are decisions made once and
hard to reverse, so making them from knowledge rather than folklore is cheap.
Legitimate — you must arbitrate. An engineer proposes a refactor for shape stability. You need to price it against alternatives in the same units, and say no with evidence rather than taste.
Illegitimate — it feels like real performance work. It is measurable, satisfying, and tractable, which makes it a magnet for effort that belongs elsewhere. Measured here: removing one forced layout from a render path is worth more than de-megamorphising 1,448 property accesses. That number is the module.
3. How it works
+==================================================================+
| HIDDEN CLASSES — structure inferred from construction order |
+==================================================================+
{} --add x--> {x} --add y--> {x,y}
Shape0 Shape1 Shape2
const a = { x: 1, y: 2 }; -> Shape0 -> Shape1 -> Shape2
const b = { y: 2, x: 1 }; -> Shape0 -> Shape1'-> Shape2'
DIFFERENT final shape
delete o.tmp -> DICTIONARY MODE (no shape at all)
+==================================================================+
| INLINE CACHES — per-access-site memory of shapes seen |
+==================================================================+
function read(o) { return o.v; }
^ this site has its own IC
1 shape seen MONOMORPHIC direct offset load fastest
2-4 shapes POLYMORPHIC short linear shape check ~equal
5+ shapes MEGAMORPHIC global stub-cache hash ~2x slower
+==================================================================+
| TIERED EXECUTION |
+==================================================================+
Ignition (interpreter)
| hot
Sparkplug (baseline) -> Maglev -> TurboFan (optimising)
^ |
+------------- DEOPTIMISE ------------+
speculation violated: a shape, type, or
element kind the compiler assumed away
The measured effects
| Effect | Measured | Notes |
|---|---|---|
| Megamorphic vs monomorphic read | 1.9× (3.8 ns vs 0.9 ns) | ratio looks alarming, absolute is 2.9 ns |
| Alternating property order | 1.14× | far smaller than folklore suggests |
delete o.tmp → dictionary mode | 11.9× | the one that genuinely bites |
o.tmp = undefined instead | 0.98× | keeps the shape; free |
| Array element kinds | 1.70× spread | ~1 ns/element; ordering counterintuitive |
4. Core terminology
| Term | Definition |
|---|---|
| Hidden class / Map / Shape | V8's internal descriptor of an object's layout |
| Transition chain | The path of shapes produced by adding properties in a given order |
| Inline cache (IC) | Per-site cache of shape → property offset |
| Monomorphic / polymorphic / megamorphic | 1 / 2–4 / 5+ shapes observed at one access site |
| Stub cache | The global hash table a megamorphic site falls back to |
| Dictionary mode | Hash-map storage after delete or too many properties; no shape, no IC |
| Element kind | Array storage specialisation: PACKED_SMI, PACKED_DOUBLE, PACKED_ELEMENTS, and holey variants |
| SMI | Small integer; a tagged 31-bit value stored without heap allocation |
| Deoptimisation | Bailing from optimised code back to the interpreter when speculation fails |
| Ignition / Sparkplug / Maglev / TurboFan | V8's interpreter and three compiler tiers |
| OSR | On-stack replacement — swapping a running function to optimised code mid-loop |
5. Mental models
The engine bets on consistency. Every optimisation is a wager that the future resembles the past: same shapes, same types, same element kinds. You do not make code fast by adding cleverness; you avoid making it slow by not surprising the engine.
Ratios seduce; absolutes decide. "2× slower" is the most misleading phrase in performance work. The megamorphic penalty is 1.9× — and 2.9 nanoseconds. Always convert to absolute cost per operation, then multiply by the real operation count, then compare against what else is on the critical path. Doing this once will kill most micro-optimisation proposals, including your own.
Micro-benchmarks lie by default. Three of the four experiments here produced confidently wrong results before they produced right ones — a shared function accumulating IC state across cases, a DOM case leaving 5,000 children for the next case, and an accumulator overflowing SMI range. Each looked plausible. Assume your first benchmark is wrong and try to break it.
Optimise the tree, then the leaf. Frontend cost lives in the network, the DOM, layout, and scheduling — in that order, by orders of magnitude. Shape optimisation is a leaf-level concern and belongs after the tree is right, which it rarely is.
6. Common misconceptions
-
"Property order matters a lot." It measured 1.14×. It is real, cheap to get right, and almost never the reason anything is slow. Treat it as hygiene, not optimisation.
-
"
deleteis fine, it's just a keyword." The one folklore item that holds: 11.9×, from dictionary mode.o.tmp = undefinedmeasured 0.98×. If you must remove keys from hot objects, rebuild the object or use aMap. -
"Megamorphic call sites are a crisis." 2.9 ns per read. You need ~1,448 of them to equal one forced layout. Fix it when it is free; never schedule work for it without a profile.
-
"
Mapis always slower than a plain object."Mapis designed for dynamic keys, never enters dictionary mode, and supports deletion without shape damage. For a genuine dictionary it is usually the better choice — the plain-object habit is a shape hazard. -
"The JIT will optimise my code." It will specialise consistent code. It cannot fix an algorithm, a request waterfall, or layout thrashing, and those dominate frontend cost.
-
"Benchmark ratios transfer between engines and versions." These are V8 implementation details on one version. JavaScriptCore and SpiderMonkey differ. Record the version with any number you keep — this module's verification log does.
7. Interview talking points
- "I convert every micro-optimisation claim to absolute nanoseconds and multiply by the real operation count before discussing it. Megamorphic access is 1.9× slower, which sounds urgent, and 2.9 ns, which usually isn't — one forced layout costs about 1,448 of them."
- "The one piece of shape folklore that survives measurement is
delete: 11.9× from dictionary mode, versus 0.98× for assigning undefined. Property order was 1.14×, which is hygiene, not a refactor." - "Shape stability matters where per-element cost multiplies — virtualized lists, canvas loops, worker transforms over millions of rows. In component code it's noise, and I'd rather the team spend that attention on request waterfalls."
- "I assume my first micro-benchmark is wrong. Building this module, three of four produced confident, plausible, backwards results from shared IC state, DOM contamination, and an accumulator type transition. Isolation per case is not optional."
- "
Mapversus plain object is a shape question, not a style question. If keys are dynamic and get deleted, a plain object drops into dictionary mode and aMapdoesn't."
8. Connections to other modules
fe-01— the perspective table is priced in the same units as scheduling work; forced layout appears in both modules as the dominant avoidable cost.fe-02— dictionary mode and shape transitions change retained size as well as speed; both are consequences of object representation.fe-14(UI algorithms) andfe-30(large-scale UI) — where per-element cost genuinely multiplies, and therefore the only places this module's findings should change a design.fe-28(workers) — structured clone cost depends on object representation; shape-stable, transferable-friendly data is the real optimisation at a worker boundary.browser-framework-internals.md§17 (V8 Integration) — cross if you want to read the IC implementation rather than measure its behaviour.