|
| 1 | +--- |
| 2 | +title: V8 Compilation — Tiers, Deopts, Explicit Compile Hints |
| 3 | +description: How V8 compiles JS through Ignition → Sparkplug → Maglev → Turbofan/Turboshaft. Covers tier-up triggers, on-stack replacement, deopt causes, what Maglev/Turbofan can and cannot inline, the truth about try/catch in optimized code, arguments object vs rest params, explicit compile hints (//# allFunctionsCalledOnLoad, Chrome 136+), --disable-optimizing-compilers and --jitless reality, eval and CSP. Load for "why does this deopt", "why is first call slow", "should I eager-compile", "is X inlined" questions. |
| 4 | +keywords: [Ignition, Sparkplug, Maglev, Turbofan, Turboshaft, Turbolev, tier-up, OSR, on-stack replacement, deoptimization, deopt, inlining, feedback vector, IC, inline cache, try catch, arguments object, rest parameters, explicit compile hints, allFunctionsCalledOnLoad, jitless, disable-optimizing-compilers, code cache, eval, new Function, CSP] |
| 5 | +audience: authoring |
| 6 | +skill: performance-v8-compilation |
| 7 | +type: skill |
| 8 | +--- |
| 9 | + |
| 10 | +# V8 Compilation — Tiers, Deopts, Explicit Compile Hints |
| 11 | + |
| 12 | +> **Skill:** `performance-v8-compilation` |
| 13 | +> **Purpose:** What V8 actually compiles, when, and what makes optimized code go away. |
| 14 | +
|
| 15 | +**Golden rule: Stability of feedback matters more than raw call count. Polymorphism that arrives early and stays consistent is fine; polymorphism that grows after the function is hot triggers deopts.** |
| 16 | + |
| 17 | +Current as of Chrome 138, May 2026. |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## The pipeline |
| 22 | + |
| 23 | +A function starts at the bottom. It tiers up only if it gets hot with stable feedback. |
| 24 | + |
| 25 | +| Tier | Description | |
| 26 | +|------|-------------| |
| 27 | +| **Ignition** | Interpreter. Parses → bytecode → executes. Fills *feedback vectors* — per-function slot arrays recording, for each IC site, what shapes and types have been seen. | |
| 28 | +| **Sparkplug** (Chrome 91+) | Baseline JIT. Single-pass bytecode → machine code. No IR, no optimizations, no inlining. ~10× faster than Ignition; ~20× slower to compile than nothing. Threshold is essentially "ran more than once." | |
| 29 | +| **Maglev** (default Chrome 117+) | Mid-tier optimizing JIT. SSA over CFG. Uses feedback to speculatively narrow types, emits shape-check + direct field load, inlines small monomorphic targets, registers dependencies on stable Maps and de-facto-constant globals. Compiles ~10× slower than Sparkplug, runs ~10× faster. This is where the bulk of "optimized" web JS lives. | |
| 30 | +| **Turbofan + Turboshaft IR** | Top tier. Turboshaft (CFG-based) replaced Sea-of-Nodes for the JS backend in Q1 2025 (v8.dev/blog/leaving-the-sea-of-nodes), roughly halving compile time. **Turbolev** — building Turboshaft graphs directly from Maglev's IR instead of from JS — is rolling out incrementally through 2025–2026. | |
| 31 | + |
| 32 | +Maglev's presence makes V8 willing to wait longer before triggering Turbofan, so functions in the "warm but not hot" band stay on Maglev. |
| 33 | + |
| 34 | +### On-Stack Replacement (OSR) |
| 35 | + |
| 36 | +A long-running loop in an already-running function can tier up mid-execution: V8 swaps the in-progress frame for an optimized frame at a loop back-edge. Shared between Maglev and Turbofan. Useful for setup loops that run many iterations on first call. |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +## What "stable feedback" actually means |
| 41 | + |
| 42 | +The framing for almost all deopt advice. Feedback stability matters more than raw call count. |
| 43 | + |
| 44 | +✅ Polymorphism that arrives early and stays consistent — a site sees three shapes in its first 100 invocations and continues to see only those three. Maglev handles it well. |
| 45 | + |
| 46 | +❌ Polymorphism that grows over time — new shapes appearing *after* Maglev/Turbofan have compiled. Optimized code's speculative checks fail, the optimized frame is discarded, the function reverts to a lower tier, and recompilation eventually runs again with the broader feedback. This cycle is the worst case. |
| 47 | + |
| 48 | +### Practical consequences |
| 49 | + |
| 50 | +✅ Construct all your shape variants early. Eagerly create the rare-but-real case during warmup so its shapes are in feedback before Maglev compiles. |
| 51 | + |
| 52 | +❌ Lazy creation of a new "kind" of object after the page has been running deopts every consumer site that saw the previous shapes. |
| 53 | + |
| 54 | +❌ Optional/conditional property addition. A property added to instances #5000+ but not to #1–4999 forks the shape tree and creates new polymorphism for downstream sites. |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## What V8 can and cannot inline |
| 59 | + |
| 60 | +Maglev and Turbofan inline call targets when: |
| 61 | + |
| 62 | +- ✅ The call site is monomorphic or low-polymorphic. |
| 63 | +- ✅ The callee is small (the budget is an internal heuristic, not stable across releases — do not hard-code expectations). |
| 64 | +- ✅ The callee is not a generator and is not `async` (the async wrapper can be split into resumable state; the synchronous body before the first await can still be inlined). |
| 65 | +- ✅ The callee does not exceed recursion limits. |
| 66 | + |
| 67 | +Maglev inlines less aggressively than Turbofan but does inline obvious small monomorphic targets. |
| 68 | + |
| 69 | +### Practical patterns |
| 70 | + |
| 71 | +✅ Keep hot primitives small — getter/setter pairs of 5–15 bytecode instructions; framework methods that are mostly one shape check + one field access. |
| 72 | + |
| 73 | +❌ Do not embed debug or dev-only branches in hot code paths even if guarded by `if (DEBUG) …`. The branch is cheap but the dead code inflates the size estimate the inliner consults. |
| 74 | + |
| 75 | +✅ Compile dev branches out (build flag) or factor them into a separate function that won't be inlined. |
| 76 | + |
| 77 | +✅ Put shared methods on the prototype (one Function object) rather than assigning per instance in the constructor (one Function object per instance — bad on a hot class). |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## try/catch is not a deopt killer in modern V8 |
| 82 | + |
| 83 | +Crankshaft (pre-2017) refused to optimize functions containing `try`/`catch`. TurboFan (2017+) lifted that with caveats. **All four current tiers handle try/catch.** The catch handler is generated as a cold path; no general "this function cannot be optimized" cost. |
| 84 | + |
| 85 | +v8.dev/blog/leaving-the-sea-of-nodes (Mar 2025) explicitly cites poor exception handling as one of the reasons Crankshaft had to be replaced. |
| 86 | + |
| 87 | +✅ Wrap user effect/callback invocations in `try`/`catch` for error isolation at framework boundaries. |
| 88 | +✅ Use `try`/`finally` for resource cleanup. |
| 89 | +⚠ Avoid stacking `try`/`catch` inside the *innermost* loop of measured hot code purely as a defensive measure — small constant cost, slight inlining-budget impact. |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +## arguments object |
| 94 | + |
| 95 | +The historical "don't touch `arguments`" rule still applies in 2026. The solution has updated. |
| 96 | + |
| 97 | +❌ Caching `arguments.length` in a local. The micro-optimization is irrelevant — the problem is using `arguments` at all. |
| 98 | + |
| 99 | +❌ The `arguments` object in non-strict, non-arrow functions is a "mapped" exotic object aliased with named parameters. Touching it forces materialization. |
| 100 | + |
| 101 | +✅ Use rest parameters: `function f(...args)`. Real Array, normal elements-kind machinery, sometimes elided by Turbofan via escape analysis when only iterated. |
| 102 | + |
| 103 | +--- |
| 104 | + |
| 105 | +## Deopt triggers (the short list) |
| 106 | + |
| 107 | +- A property previously type-checked as Smi receives a non-Smi value. |
| 108 | +- An object shape used at an IC site diverges from the shapes the compiled code expected. |
| 109 | +- A previously-stable Map transitions (e.g., a property is deleted, the object enters dictionary mode). |
| 110 | +- A global previously assumed constant is reassigned. |
| 111 | +- A class's prototype is mutated. |
| 112 | +- A `typeof` guard fails its speculative narrowing. |
| 113 | +- An out-of-bounds array read at a previously in-bounds site. |
| 114 | +- An IC transitions from polymorphic (≤4 shapes) to megamorphic (5+). |
| 115 | + |
| 116 | +Each deopt invalidates the optimized code and reverts the function to a lower tier. One-time deopts during page warmup are normal; recurring deopts are pathological. |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Explicit Compile Hints — `//# allFunctionsCalledOnLoad` |
| 121 | + |
| 122 | +**Status: shipped, default-on, Chrome 136 (April 2025).** Source: Marja Hölttä, v8.dev/blog/explicit-compile-hints, Apr 2025. |
| 123 | + |
| 124 | +### What it does |
| 125 | + |
| 126 | +V8 normally tries to defer compilation: it preparses each script to find function boundaries, then compiles each function lazily on first call. For functions that *will* be called during page load, that's wasted work — the preparse duplicates the eventual full parse, and eager compilation could have run on a background thread interleaved with network load. |
| 127 | + |
| 128 | +The magic comment placed at the top of a JavaScript file tells V8 to compile every function in that file eagerly: |
| 129 | + |
| 130 | +```js |
| 131 | +//# allFunctionsCalledOnLoad |
| 132 | + |
| 133 | +function bootstrap() { /* ... */ } |
| 134 | +function defineCustomElements() { /* ... */ } |
| 135 | +// every function in this file is eagerly parsed and compiled |
| 136 | +``` |
| 137 | + |
| 138 | +Production test cited in the post: 17 of 20 popular pages improved, average foreground parse+compile time reduced by ~630 ms. |
| 139 | + |
| 140 | +The hint forces eager **parse + bytecode generation** (Ignition-level compilation). It does **not** skip tier-up — Sparkplug/Maglev/Turbofan still happen normally on hot code. |
| 141 | + |
| 142 | +### Interaction with code caching |
| 143 | + |
| 144 | +Chrome's V8 compile cache stores eagerly-compiled bytecode on the second load. The post recommends testing with a clean user-data directory because the code cache otherwise masks the difference. In production, the first-load win is what the hint delivers; subsequent loads benefit from cached bytecode regardless. |
| 145 | + |
| 146 | +✅ Combine the hint with long-cache `Cache-Control` on framework core files. Normal HTTP caching; V8 picks it up automatically. |
| 147 | + |
| 148 | +### When to use it |
| 149 | + |
| 150 | +✅ Apply to **a framework core file** — the reactive primitive, the scheduler, the base custom-element class, the bootstrap path. Anything called on every page in the first few hundred milliseconds. |
| 151 | + |
| 152 | +❌ Do not apply to: |
| 153 | +- Large optional / lazy-loaded modules. Defeats lazy parsing entirely. |
| 154 | +- Files containing large constant tables (precomputed lookups embedded in JS). Eager parsing of the data literal is wasted work. |
| 155 | + |
| 156 | +The post's own warning: *"This feature should be used sparingly though — compiling too much will consume time and memory."* |
| 157 | + |
| 158 | +### Future direction |
| 159 | + |
| 160 | +The post states V8 plans to support per-function compile hints. At time of the post, only file-level was available. Verify current status before recommending per-function granularity — see `performance-v8-uncertain-topics`. |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## JIT-disabled scenarios |
| 165 | + |
| 166 | +Chrome 126 (June 2024) introduced `--disable-optimizing-compilers`, the V8 flag that disables Maglev and Turbofan while keeping **Ignition and Sparkplug** running. The older `--jitless` flag is stricter: Ignition only. Both modes appear in the field: |
| 167 | + |
| 168 | +- The Chrome 122+ user toggle at `chrome://settings/security` ("V8 optimizer") sets `--disable-optimizing-compilers` per site. |
| 169 | +- The enterprise policy `DefaultJavaScriptJitSetting` controls it organization-wide. |
| 170 | +- Some sandboxed configurations (certain WebView modes, locked-down enterprise machines) force jitless. |
| 171 | + |
| 172 | +**Code that depends on Maglev/Turbofan-only optimizations must still be correct and acceptably fast when only Sparkplug is running.** This includes: |
| 173 | + |
| 174 | +- Escape analysis (closure-allocation elision). |
| 175 | +- Speculative type narrowing. |
| 176 | +- Aggressive inlining. |
| 177 | +- Vectorized loops over TypedArrays. |
| 178 | +- IC-based speculative shape-check fast paths. |
| 179 | + |
| 180 | +❌ Don't recommend patterns that are *worse* without optimization than the simpler alternative would be. |
| 181 | + |
| 182 | +Source: Lamprey Labs, "Return of the JIT," documenting the Chrome 126 change. |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## eval, new Function, dynamic code |
| 187 | + |
| 188 | +- ✅ `eval` and `new Function(...)` produce code that participates normally in the tier pipeline. Not unoptimizable per se. |
| 189 | +- ❌ Blocked by strict CSP `script-src` policies that exclude `'unsafe-eval'`. A framework that depends on `new Function` won't work on CSP-strict sites. |
| 190 | +- ❌ Disabled entirely in `--jitless` configurations. |
| 191 | +- ❌ Defeat inlining (the caller cannot inline a function it doesn't statically see). |
| 192 | +- ❌ Make shape inference much harder for surrounding code. |
| 193 | + |
| 194 | +✅ Acceptable for build-time code generation that runs once. |
| 195 | +❌ Not acceptable as a runtime optimization technique. |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## Quick Reference |
| 200 | + |
| 201 | +```js |
| 202 | +//# allFunctionsCalledOnLoad // ✅ Top of framework core file, Chrome 136+ |
| 203 | + |
| 204 | +// ✅ Rest params, not arguments |
| 205 | +function effect(...args) { /* … */ } |
| 206 | + |
| 207 | +// ❌ arguments object |
| 208 | +function effect() { |
| 209 | + for (let i = 0; i < arguments.length; i++) { /* … */ } |
| 210 | +} |
| 211 | + |
| 212 | +// ✅ Methods on prototype (one Function object shared) |
| 213 | +class Signal { |
| 214 | + get() { /* … */ } |
| 215 | + set(v) { /* … */ } |
| 216 | +} |
| 217 | + |
| 218 | +// ❌ Methods assigned per-instance in constructor (one per instance) |
| 219 | +class Signal { |
| 220 | + constructor() { |
| 221 | + this.get = () => { /* … */ }; |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +// ✅ try/catch at framework boundary |
| 226 | +function runEffect(fn) { |
| 227 | + try { fn(); } catch (e) { reportError(e); } |
| 228 | +} |
| 229 | +``` |
| 230 | + |
| 231 | +--- |
| 232 | + |
| 233 | +## Primary sources |
| 234 | + |
| 235 | +- v8.dev/blog/maglev — Dec 2023 |
| 236 | +- v8.dev/blog/leaving-the-sea-of-nodes — Mar 2025 |
| 237 | +- v8.dev/blog/sparkplug — 2021 |
| 238 | +- v8.dev/blog/explicit-compile-hints — Marja Hölttä, Apr 2025 |
| 239 | +- lampreylabs.com/posts/return-of-the-jit — Chrome 126 `--disable-optimizing-compilers` |
| 240 | + |
| 241 | +--- |
| 242 | + |
| 243 | +## Related Skills |
| 244 | + |
| 245 | +| Skill | Command | Use when... | |
| 246 | +|-------|---------|-------------| |
| 247 | +| **Performance Index** | `use_skill('performance-v8-overview')` | Need the tier model summary or routing. | |
| 248 | +| **Object Model** | `use_skill('performance-v8-object-model')` | The shape-stability rules that determine whether tier-up happens cleanly. | |
| 249 | +| **Stale Advice** | `use_skill('performance-v8-stale-advice')` | Verify a remembered rule about try/catch, arguments, inlining, or deopts. | |
| 250 | +| **Uncertain Topics** | `use_skill('performance-v8-uncertain-topics')` | About to claim specific tier-up tick counts or inlining budgets — don't. | |
0 commit comments