$npx -y skills add vercel/next.js --skill v8-jitV8 JIT optimization patterns for writing high-performance JavaScript in Next.js server internals. Use when writing or reviewing hot-path code in app-render, stream-utils, routing, caching, or any per-request code path. Covers hidden classes / shapes, monomorphic call sites, inlin
| 1 | # V8 JIT Optimization |
| 2 | |
| 3 | Use this skill when writing or optimizing performance-critical code paths in |
| 4 | Next.js server internals — especially per-request hot paths like rendering, |
| 5 | streaming, routing, and caching. |
| 6 | |
| 7 | ## Background: V8's Tiered Compilation |
| 8 | |
| 9 | V8 compiles JavaScript through multiple tiers: |
| 10 | |
| 11 | 1. **Ignition** (interpreter) — executes bytecode immediately. |
| 12 | 2. **Sparkplug** — fast baseline compiler (no optimization). |
| 13 | 3. **Maglev** — mid-tier optimizing compiler. |
| 14 | 4. **Turbofan** — full optimizing compiler (speculative, type-feedback-driven). |
| 15 | |
| 16 | Code starts in Ignition and is promoted to higher tiers based on execution |
| 17 | frequency and collected type feedback. Turbofan produces the fastest machine |
| 18 | code but **bails out (deopts)** when assumptions are violated at runtime. |
| 19 | |
| 20 | The key principle: **help V8 make correct speculative assumptions by keeping |
| 21 | types, shapes, and control flow predictable.** |
| 22 | |
| 23 | ## Hidden Classes (Shapes / Maps) |
| 24 | |
| 25 | Every JavaScript object has an internal "hidden class" (V8 calls it a _Map_, |
| 26 | the spec calls it a _Shape_). Objects that share the same property names, added |
| 27 | in the same order, share the same hidden class. This enables fast property |
| 28 | access via inline caches. |
| 29 | |
| 30 | ### Initialize All Properties in Constructors |
| 31 | |
| 32 | ```ts |
| 33 | // GOOD — consistent shape, single hidden class transition chain |
| 34 | class RequestContext { |
| 35 | url: string |
| 36 | method: string |
| 37 | headers: Record<string, string> |
| 38 | startTime: number |
| 39 | cached: boolean |
| 40 | |
| 41 | constructor(url: string, method: string, headers: Record<string, string>) { |
| 42 | this.url = url |
| 43 | this.method = method |
| 44 | this.headers = headers |
| 45 | this.startTime = performance.now() |
| 46 | this.cached = false // always initialize, even defaults |
| 47 | } |
| 48 | } |
| 49 | ``` |
| 50 | |
| 51 | ```ts |
| 52 | // BAD — conditional property addition creates multiple hidden classes |
| 53 | class RequestContext { |
| 54 | constructor(url, method, headers, options) { |
| 55 | this.url = url |
| 56 | this.method = method |
| 57 | if (options.timing) { |
| 58 | this.startTime = performance.now() // shape fork! |
| 59 | } |
| 60 | if (options.cache) { |
| 61 | this.cached = false // another shape fork! |
| 62 | } |
| 63 | this.headers = headers |
| 64 | } |
| 65 | } |
| 66 | ``` |
| 67 | |
| 68 | **Rules:** |
| 69 | |
| 70 | - Assign every property in the constructor, in the same order, for every |
| 71 | instance. Use `null` / `undefined` / `false` as default values rather than |
| 72 | omitting the property. |
| 73 | - Prefer factory functions when constructing hot-path objects. A single factory |
| 74 | makes it harder to accidentally fork shapes in different call sites. |
| 75 | - Never `delete` a property on a hot object — it forces a transition to |
| 76 | dictionary mode (slow properties). |
| 77 | - Avoid adding properties after construction (`obj.newProp = x`) on objects |
| 78 | used in hot paths. |
| 79 | - Object literals that flow into the same function should have keys in the |
| 80 | same order: |
| 81 | - Use tuples for very small fixed-size records when names are not needed. |
| 82 | Tuples avoid key-order pitfalls entirely. |
| 83 | |
| 84 | ```ts |
| 85 | // GOOD — same key order, shares hidden class |
| 86 | const a = { type: 'static', value: 1 } |
| 87 | const b = { type: 'dynamic', value: 2 } |
| 88 | |
| 89 | // BAD — different key order, different hidden classes |
| 90 | const a = { type: 'static', value: 1 } |
| 91 | const b = { value: 2, type: 'dynamic' } |
| 92 | ``` |
| 93 | |
| 94 | ### Real Codebase Example |
| 95 | |
| 96 | `Span` in `src/trace/trace.ts` initializes all fields in the constructor in a |
| 97 | fixed order — `name`, `parentId`, `attrs`, `status`, `id`, `_start`, `now`. |
| 98 | This ensures all `Span` instances share one hidden class. |
| 99 | |
| 100 | ## Monomorphic vs Polymorphic vs Megamorphic |
| 101 | |
| 102 | V8's inline caches (ICs) track the types/shapes seen at each call site or |
| 103 | property access: |
| 104 | |
| 105 | | IC State | Shapes Seen | Speed | |
| 106 | | --------------- | ----------- | ------------------------------------- | |
| 107 | | **Monomorphic** | 1 | Fastest — single direct check | |
| 108 | | **Polymorphic** | 2–4 | Fast — linear search through cases | |
| 109 | | **Megamorphic** | 5+ | Slow — hash-table lookup, no inlining | |
| 110 | |
| 111 | Once an IC goes megamorphic it does NOT recover (until the function is |
| 112 | re-compiled). Megamorphic ICs also **prevent Turbofan from inlining** the |
| 113 | function. |
| 114 | |
| 115 | ### Keep Hot Call Sites Monomorphic |
| 116 | |
| 117 | ```ts |
| 118 | // GOOD — always called with the same argument shape |
| 119 | function processChunk(chunk: Uint8Array): void { |
| 120 | // chunk is always Uint8Array → monomorphic |
| 121 | } |
| 122 | |
| 123 | // BAD — called with different types at the same call site |
| 124 | function processChunk(chunk: Uint8Array | Buffer | string): void { |
| 125 | // IC becomes polymorphic/megamorphic |
| 126 | } |
| 127 | ``` |
| 128 | |
| 129 | **Practical strategies:** |
| 130 | |
| 131 | - Normalize inputs at the boundary (e.g. convert `Buffer` → `Uint8Array` |
| 132 | once) and keep in |