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.


0. Orientation

The 60-second version

JavaScript is specified as if every object were a hash map: a bag of string keys pointing at values, mutable at any moment. If engines implemented it that way, every o.x would be a hash lookup and the language would be roughly an order of magnitude slower than it is.

They do not. V8 guesses that your objects have structure and compiles as if they did:

  • It assigns each object a hidden class describing its layout, so o.x becomes a load from a fixed offset instead of a hash lookup.
  • It caches, at each property-access site in your source, which layouts that site has seen — so a site that only ever sees one layout compiles to a single machine instruction plus a check.
  • It specialises arrays by content, so an array of small integers is stored unboxed.
  • It compiles hot functions against those assumptions, and throws the compiled code away the moment an assumption is violated.

Every one of those is a bet on consistency. You do not make code fast by being clever; you avoid making it slow by not surprising the engine.

And then the twist

All of that is true, reproducible, and — in nearly all frontend code — irrelevant. The measured megamorphic penalty is 2.9 nanoseconds per property read. One forced synchronous layout costs 4,200 ns. So:

Removing one forced layout from a render path is worth more than de-megamorphising 1,448 property accesses.

That sentence is the module. Everything else is the evidence for it, and the vocabulary you need to say it credibly to someone who has just proposed a two-week shape-stability refactor.

What you should be able to do after this module

CapabilityEvidence you have it
Price a micro-optimisationConvert ratio → absolute ns → real op count → % of frame budget, in one sitting
Say no with evidenceDecline a shape refactor by naming what it costs and what else the same week buys
Recognise the real exceptionsIdentify the five situations (§6.1) where the arithmetic genuinely flips
Choose a representationDecide Map vs object, array-of-objects vs object-of-arrays, from mechanism
Distrust a benchmarkAssume your first micro-benchmark is wrong, and know the three ways it lies

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.

Why the engine is built this way

The tension the engine is resolving:

The language promisesWhich naively impliesSo the engine bets
Objects are dynamic bags of keysA hash lookup per property accessMost objects at a site share a layout
Any property can be added or removed at any timeNo fixed offsets are safeLayouts change rarely, and transitions can be cached
Values are untypedEvery arithmetic op needs a type checkThe types seen so far are the types you will keep seeing
Arrays are just objects with numeric keysBoxed, heap-allocated elementsArrays are usually homogeneous and dense

Each bet is paid for with a guard — a cheap runtime check that the assumption still holds. Fast code is code where guards pass. The entire performance model is: speculate, guard, and bail out correctly when the guard fails.

This is why the advice is never "write clever code." It is "write code where the guard passes" — build objects the same way, keep arrays homogeneous, don't delete.


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.

The failure mode this module exists to prevent

The pattern is specific and it recurs at every company:

  1. A senior engineer reads about hidden classes. The material is genuinely good and genuinely true.
  2. They find real megamorphic sites in the codebase. They are really there.
  3. They propose a refactor. It is technically correct, it is measurable, and it will produce a green benchmark.
  4. It ships. Nothing the user can perceive changes, because the saving was 0.9% of one frame.
  5. Meanwhile the request waterfall, the unvirtualized 5,000-row list, and the 200 KB JSON.parse on the same critical path are untouched.

Nobody in that story did anything wrong except choose. The risk this module addresses is opportunity cost, not slow code — and the antidote is arithmetic, delivered early enough to redirect the effort rather than as a post-mortem.


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

3.1 Hidden classes are a transition tree, not a label

The mechanism people miss: a hidden class is not computed from the object's final contents. It is arrived at by walking a transition tree from the empty shape, one property at a time, in the order you added them.

                     Shape0  {}
                    /          \
        +x         /            \        +y
                  v              v
          Shape1 {x}          Shape1' {y}
              |                    |
        +y    |                    | +x
              v                    v
        Shape2 {x,y}         Shape2' {y,x}

{x:1, y:2} and {y:2, x:1} contain identical data and end at different shapes, because they walked different branches. That is why construction order matters at all — and it is also why the effect is smaller than folklore claims, because two shapes at a site is still monomorphic-speed.

Two mechanisms hang off this tree that are worth knowing by name:

Transitions are shared and cached. The tree is global, so the second object built as {x, y} does not recompute anything — it follows the existing edges. This is why "build objects the same way" is cheap advice: the fast path is already warm.

Dictionary mode is falling off the tree entirely. When you delete a property, V8 cannot represent the result as a point in the transition tree (there is no "remove" edge), so the object is converted to an actual hash map. It now has no shape, so every IC that touches it degrades, and it stays that way. This is why delete measured 11.9× while property order measured 1.14× — they are not the same kind of mistake at all. One picks a different branch of the tree; the other leaves the tree.

3.2 Inline caches, and the shape of the cliff

An IC lives at a source location, not on an object. function read(o) { return o.v } has exactly one IC no matter how many objects flow through it — which is why a single generic helper called from twelve places is the classic way to create a megamorphic site out of perfectly consistent data.

distinct shapesIC statemedian msns/readvs monomorphic
1monomorphic5.301.7671.00×
2polymorphic5.301.7671.00×
4polymorphic6.502.1671.23×
8megamorphic10.403.4671.96×
16megamorphic10.303.4331.94×

The access site is character-identical in every row; only shape variety differs.

The curve is not linear, and its shape is what makes it actionable:

1 shape   1.00x
2 shapes  1.00x   <- free
4 shapes  1.23x
8 shapes  1.96x   <- cliff crossed
16 shapes 1.94x   <- flat; already in the stub cache
  • Going from 1 to 4 shapes is nearly free. Refactoring a site to be strictly monomorphic is usually not worth it. This kills a lot of proposed work.
  • Going from 8 to 80 shapes costs nothing further. Once megamorphic, "reducing shape variety" buys nothing unless you get back under the threshold — rarely achievable in code that legitimately handles heterogeneous data.

So the only actionable version is: avoid crossing the cliff in code that is genuinely hot. Once crossed, stop optimising and go read §6.2.

3.3 Element kinds — array specialisation

Arrays get their own specialisation lattice, and transitions are one-way: once an array becomes holey it never becomes packed again, even if you fill the hole.

   PACKED_SMI  ──►  PACKED_DOUBLE  ──►  PACKED_ELEMENTS
        │                 │                    │
        ▼                 ▼                    ▼
   HOLEY_SMI   ──►  HOLEY_DOUBLE   ──►  HOLEY_ELEMENTS

   more specific ─────────────────────────► more general
   (one-way: you can descend, never climb back)

Measured, 5,000,000 elements summed per rep, fresh page per case:

element kindmedian msns/elemvs SMI
PACKED_SMI — all small integers7.801.5601.00×
PACKED_DOUBLE — all doubles4.700.9400.60×
PACKED_ELEMENTS — 1% nulls7.701.5400.99×
HOLEY_SMI — 1% holes8.001.6001.03×

Spread: 1.70×, or about 1 ns per element.

The practical rules that follow: prefer new Array(n).fill(0) or Array.from({length: n}) over new Array(n) (which is born holey); don't assign past the end of an array; and don't mix types in an array you intend to iterate hot.

And one honest anomaly. Doubles measured faster than small integers, which is backwards from the usual account. The first hypothesis — the accumulator overflowing SMI range mid-loop — was tested and rejected: constraining values to i % 100 keeps the sum well inside SMI range and the ordering did not change. The most plausible remaining explanation is that SMI addition carries a per-accumulate overflow check that double addition does not.

It is recorded as unexplained, and that is deliberate. It is ~0.6 ns/element either way, which is precisely the magnitude this module argues you should stop investigating. A curriculum that quietly deleted the inconvenient result would be teaching the opposite of its own thesis.

3.4 Tiering and deoptimisation

V8 runs your function at escalating tiers, and each tier costs more to produce and runs faster:

TierWhat it doesWhen
IgnitionBytecode interpreter; collects type feedbackAlways, first
SparkplugFast non-optimising baseline compilerQuickly, for anything called more than trivially
MaglevMid-tier optimiser; speculates, compiles fastWarm functions
TurboFanFull optimiser; aggressive speculation and inliningHot functions

Deoptimisation is the bail-out: an optimised function hits a case its guards rule out — an unexpected shape, a type that is not what feedback predicted, an array that turned holey — and V8 discards the optimised code and resumes in the interpreter, mid-function, via on-stack replacement.

One deopt is cheap. The pathological case is a deopt loop: a function optimised, deoptimised, re-optimised, and deoptimised again, forever — which is worse than never optimising, because you pay compilation repeatedly and never run the fast code.

The honest field note: these are invisible without --trace-deopt on a locally-built V8, which means a frontend engineer will essentially never find one. That is itself the argument for not speculating about JIT behaviour in code review. If you cannot observe it, you cannot claim it.

3.5 The measured effects, together

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 treeThe global tree of shapes; adding a property walks an edge
Transition chainThe path of shapes produced by adding properties in a given order
Inline cache (IC)Per-source-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
Packed vs holeyWhether the array has gaps; the transition to holey is one-way
GuardThe cheap runtime check that a speculation still holds; fast code is code where guards pass
DeoptimisationBailing from optimised code back to the interpreter when a guard fails
Deopt loopRepeated optimise/deopt cycling — worse than never optimising
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.

An IC belongs to a line of code, not to an object. This is the mental flip that makes shape problems findable. Ask "which site sees many shapes?" not "which object is weird?" — the answer is almost always a generic helper or an abstraction layer with many callers.

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.

Unexplained results stay unexplained. The double-vs-SMI anomaly is in this module with its failed hypothesis attached. A result you cannot explain is a known unknown, which is a strictly better state than a plausible story you did not test — and modelling that is more valuable to a team than any individual number here.


6. Applied: the decisions this module actually makes

6.1 When this genuinely matters

The honest list. If your situation is not on it, §6.2 applies and you should be working on something else.

SituationWhy the arithmetic changesExample
Per-element cost × 10⁵–10⁷3 ns becomes 30 msvirtualized table over 10M cells (fe-30)
Per-frame budget of 16.7 mssmall constants recur 60×/scanvas/WebGL render loop, physics
Hot library internalsyour users' users pay itreconciler, parser, diff, state store
Worker transformsdominated by data representationcolumnar processing, structured clone (fe-28)
Data representation decisionsone-way and expensive to reversearray-of-objects vs object-of-arrays; Map vs object

Everywhere else — component code, event handlers, data fetching, form logic, business rules — the effects are real and swamped by DOM, layout, network and scheduling.

The test to apply before starting: what is the operation count, what is the absolute per-op saving, and what else is on the same critical path? If you cannot answer all three, you are not ready to spend anyone's time.

6.2 The perspective table — the module's actual deliverable

operationns each
monomorphic property read0.9
megamorphic property read3.8
createElement + textContent + appendChild360
addEventListener + removeEventListener130
style write + getBoundingClientRect (forced layout)4,200
JSON.parse of a ~200 KB API response396,000

Megamorphic penalty: 2.9 ns per read. So, property reads you must de-megamorphise to save the cost of one of the following:

operationequivalent reads
addEventListener + removeEventListener45
create + append one element124
one forced layout1,448
one JSON.parse of a 200 KB response136,552

Bring this table to the next discussion about object shapes. It ends most of them in under a minute, and it does so without dismissing the other engineer's correct technical claim — which is what makes it usable more than once.

6.3 The arithmetic that ends the debate

A proposal to "fix megamorphic access in the render path" sounds serious. Price it:

penalty            2.9 ns per read
reads per render   50,000            (generous for a component tree)
saving             0.145 ms per render

0.145 ms against a 16.7 ms frame budget: 0.9%. Now price the alternative on the same path — at 800 elements interleaving reads and writes, fe-01 measured layout thrashing at 86 ms of avoidable cost.

The shape refactor buys 0.145 ms. Removing the thrash buys 86 ms. Same engineer, same week, 590× the return.

This arithmetic is reusable: convert to absolutes, multiply by real counts, compare against alternatives on the same path. Most micro-optimisation proposals do not survive it — including ones you will be tempted by.

6.4 delete — the exception worth acting on

11.9×, and the fix is free. It is worth a lint rule where objects are hot, because it is the rare case where the folklore, the measurement, and the cost of compliance all align.

// dictionary mode: 11.93x
const o = { x, y, z, tmp: 0 }; delete o.tmp;

// shape preserved: 0.98x
const o = { x, y, z, tmp: 0 }; o.tmp = undefined;

// no phantom key at all — usually the right answer
const { tmp, ...rest } = o;

// dynamic keys that get deleted: this is what Map is for
const m = new Map(); m.set(k, v); m.delete(k);

The third form allocates a new object, which is the correct trade in almost all frontend code and the wrong one in a hot loop. The fourth is the structural answer: if keys are dynamic and deleted, a plain object is being used as a dictionary and Map is the type that was designed for it.

6.5 Map deserves rehabilitation

The folklore ("objects are faster than Maps") comes from benchmarks with static string keys, where an object's shape is stable and its IC monomorphic. That is the case Map was never for.

UseBetter choiceWhy
Fixed, known keysplain objectstable shape, monomorphic ICs
Dynamic keys from dataMapno shape churn, no dictionary-mode cliff
Keys added and deletedMapdelete on an object costs 11.9×
Non-string keysMapobjects coerce keys to strings
Needs size / iteration orderMap.size is O(1); insertion order guaranteed
Keyed by object lifetimeWeakMapsee fe-02

A plain object accumulating user IDs as keys is a shape hazard and a dictionary-mode candidate and a leak candidate. Three separate modules point at the same refactor — which is the strongest form of evidence available for a representation change.

6.6 Reviewing a diff — what is and is not worth a comment

Worth a comment (free to fix, real effect):

  1. delete on an object in a hot path. 11.9×, one-line fix.
  2. new Array(n) without a fill, then indexed writes. Born holey, one-way.
  3. A plain object used as a dynamic dictionary with deletion. Wrong type; see §6.5.
  4. Conditionally-added properties producing divergent shapes in a constructor (if (opts.x) this.x = ...). Initialise all fields unconditionally; it is free.

Not worth a comment (real, but noise):

  • Property order between two object literals. 1.14×.
  • A site seeing 2–4 shapes. Measured free.
  • A site already megamorphic, unless you can get back under the threshold.
  • Any of it in component code, event handlers, or data-fetching logic.

Never worth a comment (unobservable):

  • Speculation about deoptimisation. You cannot see it without --trace-deopt, so nobody in the review can confirm or refute it, and the discussion cannot terminate.

6.7 Trusting your own measurements

Three harness failures, all producing plausible wrong answers:

  1. Shared IC state across cases. One page for all four element kinds meant the shared sum accumulated feedback across kinds: the first case paid JIT warm-up, later cases inherited a warm-but-polymorphic function. Reported doubles as 4× faster than SMIs. Fix: fresh page per case.
  2. DOM contamination between cases. createAppend left 5,000–20,000 children in the host element, so every subsequent getBoundingClientRect measured layout over a huge subtree. The script did not produce a wrong number — it timed out, which was luckier than the alternative. Fix: fresh page per case, and reset the DOM inside the case.
  3. Accumulator type transition. Summing raw i over 5M elements reaches ~1.25e13, overflowing SMI range and transitioning the accumulator to a double partway through the loop — attributing a cost to the array that belonged to the sum variable. Fix: constrain values so the accumulator stays in range. (This fix did not change the ordering, which is how it was ruled out as the cause of the double/SMI anomaly.)

Plus one mechanical trap worth recording: backticks inside comments in a page template literal terminate the template, twice producing syntax errors far from the real line.

Rule: every micro-benchmark needs a case whose answer you already know. If the harness cannot reproduce a result you are certain of, it cannot be trusted on one you are not.

6.8 What breaks at scale

  • Deoptimisation loops. Worse than never optimising, and invisible without --trace-deopt.
  • Benchmarks that stop resembling the application. A micro-benchmark runs one shape through one site; production runs your component with props from twelve call sites. Shape-stability results routinely fail to reproduce in the app, and the benchmark is the thing that is wrong.
  • Framework internals move the ground. Which objects a framework allocates per render, and whether they are shape-stable, changes between minor versions. Application-level optimisations built on those assumptions silently expire.
  • Engine divergence. These are V8 behaviours. JavaScriptCore and SpiderMonkey differ in IC design, element-kind handling, and tiering. Any optimisation justified by numbers from one engine needs a statement about the other two before it becomes a standard.
  • The opportunity cost is the real risk. The failure mode of this module is not writing slow code; it is a senior engineer spending a sprint on shapes while a request waterfall, an unindexed list render, and a 200 KB JSON parse sit untouched on the same critical path.

7. 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. "Reducing shape variety always helps." The curve is a cliff, not a slope. 1→4 shapes is free; 8→80 costs nothing further. Only crossing back under the threshold changes anything.

  8. "An inline cache belongs to the object." It belongs to the source location. One generic helper called from twelve places is the standard way to make a megamorphic site out of twelve perfectly consistent callers.

  9. "Filling the hole makes the array packed again." Element-kind transitions are one-way. Once holey, always holey — which is why new Array(n) followed by indexed writes is a different thing from new Array(n).fill(0).

  10. "My code deoptimises." Possibly, and you cannot know without --trace-deopt. An unobservable claim cannot be the basis of a review decision.


8. 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. So I'd take the lint rule and decline the refactor."
  • "The IC curve is a cliff, not a slope — 1 to 4 shapes measured free, and 8 to 16 measured identical. That changes the advice completely: there's no value in making a site more monomorphic, only in not crossing the threshold in genuinely hot code."
  • "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."
  • "One of our results — doubles summing faster than small integers — we couldn't explain. We tested and rejected the obvious hypothesis and left it recorded as unexplained, because it was worth 0.6 ns per element and this is exactly the magnitude the module argues you should stop chasing. I'd rather a team hold a known unknown than an untested story."

9. 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 — the other half of V8's object model. Shapes govern access cost; retention governs lifetime. Dictionary mode and shape transitions change retained size as well as speed.
  • 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.
  • bi-05-bindings-v8 — the same machinery from the source side: how Blink's bindings layer creates wrappers and what that costs per DOM property access.
  • browser-framework-internals.md §17 (V8 Integration) — cross if you want to read the IC implementation rather than measure its behaviour.