Measured Results

Chrome for Testing 149.0.7827.55 (arm64 macOS) via playwright-core/CDP. Reproduce: cd ../src && npm run all. Medians of 7 reps after 3 warm-up runs.

These are V8 implementation details on one version, not language guarantees.


1. Inline caches — 3,000,000 property reads per rep

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.

Note the shape of the curve. Nothing happens from 1→2. The cliff is between 4 and 8 — the polymorphic→megamorphic boundary — and past 8 it flattens: 16 shapes is no worse than 8, because the site has already fallen back to the stub cache.

Absolute penalty: 1.7 ns per read here, 2.9 ns in the perspective run (different iteration count and JIT tier).


2. Hidden classes — 400,000 objects created + read per rep

variantmedian msvs A
A — same property order5.601.00×
B — alternating property order6.401.14×
C — delete o.tmp (dictionary mode)66.8011.93×
D — o.tmp = undefined instead5.500.98×

The folklore is mostly wrong and one part is right. Property order (the part everyone repeats) is 1.14×. delete (the part people wave away) is 11.9×, and the fix is free.


3. Array element kinds — 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×.

Doubles measured faster than small integers, and I did not fully isolate why. The first hypothesis — the accumulator overflowing SMI range and transitioning to double mid-loop — was tested and rejected: constraining values to i % 100 keeps the sum near 2.5e8, well inside SMI range, and the ordering did not change. The most plausible remaining explanation is that SMI addition carries an overflow check per accumulate which double addition does not.

Recorded as unexplained. It is ~0.6 ns/element either way, which is why it did not justify further investigation — a judgement this module explicitly endorses, and an open question in the learning log rather than a claim.


4. Perspective — what these optimisations are worth

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.

Property reads you must de-megamorphise to save the cost of one:

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

Removing one forced layout from a render path is worth more than de-megamorphising 1,448 property accesses. One avoided 200 KB JSON parse is worth more than 136,000.

This table is the module's deliverable. Bring it to the next discussion about object shapes.


Harness failure modes — three, 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 measured 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 finding 3.)

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.