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

EffectMeasuredNotes
Megamorphic vs monomorphic read1.9× (3.8 ns vs 0.9 ns)ratio looks alarming, absolute is 2.9 ns
Alternating property order1.14×far smaller than folklore suggests
delete o.tmp → dictionary mode11.9×the one that genuinely bites
o.tmp = undefined instead0.98×keeps the shape; free
Array element kinds1.70× spread~1 ns/element; ordering counterintuitive

4. Core terminology

TermDefinition
Hidden class / Map / ShapeV8's internal descriptor of an object's layout
Transition chainThe path of shapes produced by adding properties in a given order
Inline cache (IC)Per-site cache of shape → property offset
Monomorphic / polymorphic / megamorphic1 / 2–4 / 5+ shapes observed at one access site
Stub cacheThe global hash table a megamorphic site falls back to
Dictionary modeHash-map storage after delete or too many properties; no shape, no IC
Element kindArray storage specialisation: PACKED_SMI, PACKED_DOUBLE, PACKED_ELEMENTS, and holey variants
SMISmall integer; a tagged 31-bit value stored without heap allocation
DeoptimisationBailing from optimised code back to the interpreter when speculation fails
Ignition / Sparkplug / Maglev / TurboFanV8's interpreter and three compiler tiers
OSROn-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

  1. "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.

  2. "delete is fine, it's just a keyword." The one folklore item that holds: 11.9×, from dictionary mode. o.tmp = undefined measured 0.98×. If you must remove keys from hot objects, rebuild the object or use a Map.

  3. "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.

  4. "Map is always slower than a plain object." Map is 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.

  5. "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.

  6. "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."
  • "Map versus 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 a Map doesn'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) and fe-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.