Analysis

When this actually matters

The honest list. If your situation is not on it, the perspective table 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.

Ratio vs absolute: the arithmetic that ends most debates

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:

one forced layout removed   4,200 ns = 0.0042 ms

...which sounds worse, until you count how many forced layouts a thrashing render path performs. At 800 elements interleaving reads and writes, fe-01 measured 86 ms. 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.

delete is 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.

The polymorphic→megamorphic cliff

The IC curve is not linear, and the shape of it changes what you do:

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

Two consequences:

  • Going from 1 to 4 shapes is nearly free. Refactoring to make a site strictly monomorphic is usually not worth it.
  • Going from 8 to 80 shapes costs nothing further. Once a site is megamorphic, "reducing" shape variety buys nothing unless you get back under the threshold — which is 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 the perspective table.

Why 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.

What breaks at scale

  • Deoptimisation loops. A function repeatedly optimised then deoptimised by an unexpected type is far worse than never optimising. These are invisible without --trace-deopt, which means a frontend engineer will essentially never find one — which is itself the argument for not speculating about JIT behaviour in code review.
  • 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 from a benchmark 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, is a framework implementation detail that changes between minor versions. Building application-level optimisations on assumptions about it creates work that silently expires.
  • Engine divergence. These are V8 behaviours. Safari (JavaScriptCore) and Firefox (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.