Crux Read real JS snippets and a deopt-trace line, predict the V8 behaviour — hidden-class breakage, monomorphic-to-megamorphic drift, and deopt triggers — and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
V8 perf bugs hide in ordinary-looking code. Read each snippet and trace line, predict what V8 does to the hidden class, the IC, or the optimised tier — then choose the fix a senior engineer makes first, before touching a flag.
Goal
Practise the loop you run in every V8 incident: read the hot path, predict where the shape or type instability comes from, and reach for the upstream fix that keeps the call site monomorphic and the values type-stable.
Snippet 1 — the late property
class Point { constructor(x, y) { this.x = x; this.y = y; }}function build(coords) { const out = []; for (const c of coords) { const p = new Point(c.x, c.y); if (c.label) p.label = c.label; // only sometimes out.push(p); } return out;}// later, in a hot loop:function sumX(points) { let s = 0; for (const p of points) s += p.x; // the hot IC site return s;}
Quiz
Completed
Some points get a .label added after construction, some do not. What happens at the p.x access site in sumX, and what is the fix?
Heads-up The IC keys off the whole hidden class, not the property being read. Adding .label transitions the object to a different class, so the p.x site now sees two classes and goes polymorphic.
Heads-up Only two hidden classes are in play (with and without label), so the site is polymorphic, not megamorphic. Megamorphic needs 5+ classes — but the fix is the same: unify the shape.
Heads-up There is no GOGC in V8, and GC has nothing to do with this — the problem is hidden-class divergence at the IC site, fixed by declaring label up front.
Snippet 2 — the dictionary trap
function makeConfig(overrides) { const cfg = { host: 'localhost', port: 8080, retries: 3 }; for (const key of Object.keys(overrides)) { cfg[key] = overrides[key]; } delete cfg.retries; // "clean up" an unused default return cfg;}function readPort(cfg) { return cfg.port; // hot path, called per request}
Quiz
Completed
readPort is on the per-request hot path. What does the delete in makeConfig do to it, and what is the fix?
Heads-up The tiny memory saving is dwarfed by the cost: delete drops the object out of fast-property mode permanently, so every subsequent read of any property goes through the slow generic path.
Heads-up Fast-property objects read at a precomputed offset regardless of property count; there is no scan. delete replaces that with a hashmap lookup, which is strictly slower.
Heads-up The keyed assignment loop can churn hidden classes, but delete is the decisive defect: it tips cfg into dictionary mode, from which V8 cannot recover without a fresh object.
Snippet 3 — the Smi overflow
function checksum(ids) { let acc = 0; for (let i = 0; i < ids.length; i++) { acc = (acc * 31 + ids[i]); // grows fast for large arrays } return acc;}
Quiz
Completed
checksum is TurboFan-optimised and fast for small inputs, but on large arrays it suddenly deopts and slows down. What is the V8-level cause and the fix?
Heads-up An in-bounds indexed loop is exactly the pattern TurboFan optimises well. The deopt reason here is the Smi-to-HeapNumber transition of acc, not bounds checking.
Heads-up Integer multiply stays Smi while the result fits ±2³¹; V8 does optimise it. The deopt happens only once acc overflows that range and boxes into a HeapNumber.
Heads-up The slowdown localises to this function after a deopt event, not a global pause. The trigger is the numeric-type transition of acc, which the | 0 coercion prevents.
Snippet 4 — the deopt trace
[marking 0x... <JSFunction processItem> for optimization using TurboFan][deoptimizing (DEOPT eager): begin <JSFunction processItem> (opt #42) @14, ;;; deoptimize at <main.js:42:18>, wrong map][marking 0x... <JSFunction processItem> for optimization using TurboFan][deoptimizing (DEOPT eager): begin <JSFunction processItem> (opt #43) @14, ;;; deoptimize at <main.js:42:18>, wrong map][marking 0x... <JSFunction processItem> for optimization using TurboFan][deoptimizing (DEOPT eager): begin <JSFunction processItem> (opt #44) @14, ;;; deoptimize at <main.js:42:18>, wrong map]
Quiz
Completed
Reading this --trace-deopt output, which statement is correct?
Heads-up 'map' is V8's internal name for a hidden class, not a debug source map. 'wrong map' means the object's hidden class did not match the one TurboFan speculated on.
Heads-up Re-marking immediately after each deopt on the same line is the death-spiral signature, not health. A stable optimised function is marked once and stays optimised.
Heads-up These three are consecutive opt #42/#43/#44 cycles on the same line — a loop, not warm-up. Warm-up deopts stop once feedback stabilises; this never stabilises because the shape keeps changing.
Recap
Every V8 incident reads the same way in code and traces: a conditionally-added property splits one array into two hidden classes and pushes a hot site polymorphic; delete tips an object into permanent dictionary mode; an accumulator that overflows the Smi range boxes into a HeapNumber and deopts a TurboFan loop; and a repeating ‘wrong map’ deopt at one source position is the deopt-loop signature. Diagnose from the trace, then fix upstream in the data model — unify the shape, drop the delete, clamp the numeric range — before you ever reach for a flag.