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.xbecomes 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
| Capability | Evidence you have it |
|---|---|
| Price a micro-optimisation | Convert ratio → absolute ns → real op count → % of frame budget, in one sitting |
| Say no with evidence | Decline a shape refactor by naming what it costs and what else the same week buys |
| Recognise the real exceptions | Identify the five situations (§6.1) where the arithmetic genuinely flips |
| Choose a representation | Decide Map vs object, array-of-objects vs object-of-arrays, from mechanism |
| Distrust a benchmark | Assume 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 promises | Which naively implies | So the engine bets |
|---|---|---|
| Objects are dynamic bags of keys | A hash lookup per property access | Most objects at a site share a layout |
| Any property can be added or removed at any time | No fixed offsets are safe | Layouts change rarely, and transitions can be cached |
| Values are untyped | Every arithmetic op needs a type check | The types seen so far are the types you will keep seeing |
| Arrays are just objects with numeric keys | Boxed, heap-allocated elements | Arrays 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:
- A senior engineer reads about hidden classes. The material is genuinely good and genuinely true.
- They find real megamorphic sites in the codebase. They are really there.
- They propose a refactor. It is technically correct, it is measurable, and it will produce a green benchmark.
- It ships. Nothing the user can perceive changes, because the saving was 0.9% of one frame.
- Meanwhile the request waterfall, the unvirtualized 5,000-row list, and the 200 KB
JSON.parseon 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 shapes | IC state | median ms | ns/read | vs monomorphic |
|---|---|---|---|---|
| 1 | monomorphic | 5.30 | 1.767 | 1.00× |
| 2 | polymorphic | 5.30 | 1.767 | 1.00× |
| 4 | polymorphic | 6.50 | 2.167 | 1.23× |
| 8 | megamorphic | 10.40 | 3.467 | 1.96× |
| 16 | megamorphic | 10.30 | 3.433 | 1.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 kind | median ms | ns/elem | vs SMI |
|---|---|---|---|
PACKED_SMI — all small integers | 7.80 | 1.560 | 1.00× |
PACKED_DOUBLE — all doubles | 4.70 | 0.940 | 0.60× |
PACKED_ELEMENTS — 1% nulls | 7.70 | 1.540 | 0.99× |
HOLEY_SMI — 1% holes | 8.00 | 1.600 | 1.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:
| Tier | What it does | When |
|---|---|---|
| Ignition | Bytecode interpreter; collects type feedback | Always, first |
| Sparkplug | Fast non-optimising baseline compiler | Quickly, for anything called more than trivially |
| Maglev | Mid-tier optimiser; speculates, compiles fast | Warm functions |
| TurboFan | Full optimiser; aggressive speculation and inlining | Hot 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
| 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 tree | The global tree of shapes; adding a property walks an edge |
| Transition chain | The path of shapes produced by adding properties in a given order |
| Inline cache (IC) | Per-source-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 |
| Packed vs holey | Whether the array has gaps; the transition to holey is one-way |
| Guard | The cheap runtime check that a speculation still holds; fast code is code where guards pass |
| Deoptimisation | Bailing from optimised code back to the interpreter when a guard fails |
| Deopt loop | Repeated optimise/deopt cycling — worse than never optimising |
| 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.
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.
| Situation | Why the arithmetic changes | Example |
|---|---|---|
| Per-element cost × 10⁵–10⁷ | 3 ns becomes 30 ms | virtualized table over 10M cells (fe-30) |
| Per-frame budget of 16.7 ms | small constants recur 60×/s | canvas/WebGL render loop, physics |
| Hot library internals | your users' users pay it | reconciler, parser, diff, state store |
| Worker transforms | dominated by data representation | columnar processing, structured clone (fe-28) |
| Data representation decisions | one-way and expensive to reverse | array-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
| operation | ns each |
|---|---|
| monomorphic property read | 0.9 |
| megamorphic property read | 3.8 |
createElement + textContent + appendChild | 360 |
addEventListener + removeEventListener | 130 |
style write + getBoundingClientRect (forced layout) | 4,200 |
JSON.parse of a ~200 KB API response | 396,000 |
Megamorphic penalty: 2.9 ns per read. So, property reads you must de-megamorphise to save the cost of one of the following:
| operation | equivalent reads |
|---|---|
addEventListener + removeEventListener | 45 |
| create + append one element | 124 |
| one forced layout | 1,448 |
one JSON.parse of a 200 KB response | 136,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.
| Use | Better choice | Why |
|---|---|---|
| Fixed, known keys | plain object | stable shape, monomorphic ICs |
| Dynamic keys from data | Map | no shape churn, no dictionary-mode cliff |
| Keys added and deleted | Map | delete on an object costs 11.9× |
| Non-string keys | Map | objects coerce keys to strings |
| Needs size / iteration order | Map | .size is O(1); insertion order guaranteed |
| Keyed by object lifetime | WeakMap | see 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):
deleteon an object in a hot path. 11.9×, one-line fix.new Array(n)without a fill, then indexed writes. Born holey, one-way.- A plain object used as a dynamic dictionary with deletion. Wrong type; see §6.5.
- 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:
- Shared IC state across cases. One page for all four element kinds meant the shared
sumaccumulated 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. - DOM contamination between cases.
createAppendleft 5,000–20,000 children in the host element, so every subsequentgetBoundingClientRectmeasured 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. - Accumulator type transition. Summing raw
iover 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
-
"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.
-
"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.
-
"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.
-
"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 fromnew Array(n).fill(0). -
"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."
- "
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." - "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) 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.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.