| name | v8-jit |
| description | V8 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, inline caches, megamorphic deopt, closure allocation, array packing, and profiling with --trace-opt / --trace-deopt.
|
| user-invocable | false |
| metadata | {"internal":true} |
V8 JIT Optimization
Use this skill when writing or optimizing performance-critical code paths in
Next.js server internals — especially per-request hot paths like rendering,
streaming, routing, and caching.
Background: V8's Tiered Compilation
V8 compiles JavaScript through multiple tiers:
- Ignition (interpreter) — executes bytecode immediately.
- Sparkplug — fast baseline compiler (no optimization).
- Maglev — mid-tier optimizing compiler.
- Turbofan — full optimizing compiler (speculative, type-feedback-driven).
Code starts in Ignition and is promoted to higher tiers based on execution
frequency and collected type feedback. Turbofan produces the fastest machine
code but bails out (deopts) when assumptions are violated at runtime.
The key principle: help V8 make correct speculative assumptions by keeping
types, shapes, and control flow predictable.
Hidden Classes (Shapes / Maps)
Every JavaScript object has an internal "hidden class" (V8 calls it a Map,
the spec calls it a Shape). Objects that share the same property names, added
in the same order, share the same hidden class. This enables fast property
access via inline caches.
Initialize All Properties in Constructors
class RequestContext {
url: string
method: string
headers: Record<string, string>
startTime: number
cached: boolean
constructor(url: string, method: string, headers: Record<string, string>) {
this.url = url
this.method = method
this.headers = headers
this.startTime = performance.now()
this.cached = false
}
}
class RequestContext {
constructor(url, method, headers, options) {
this.url = url
this.method = method
if (options.timing) {
this.startTime = performance.now()
}
if (options.cache) {
this.cached = false
}
this.headers = headers
}
}
Rules:
- Assign every property in the constructor, in the same order, for every
instance. Use
null / undefined / false as default values rather than
omitting the property.
- Prefer factory functions when constructing hot-path objects. A single factory
makes it harder to accidentally fork shapes in different call sites.
- Never
delete a property on a hot object — it forces a transition to
dictionary mode (slow properties).
- Avoid adding properties after construction (
obj.newProp = x) on objects
used in hot paths.
- Object literals that flow into the same function should have keys in the
same order:
- Use tuples for very small fixed-size records when names are not needed.
Tuples avoid key-order pitfalls entirely.
const a = { type: 'static', value: 1 }
const b = { type: 'dynamic', value: 2 }
const a = { type: 'static', value: 1 }
const b = { value: 2, type: 'dynamic' }
Real Codebase Example
Span in src/trace/trace.ts initializes all fields in the constructor in a
fixed order — name, parentId, attrs, status, id, _start, now.
This ensures all Span instances share one hidden class.
Monomorphic vs Polymorphic vs Megamorphic
V8's inline caches (ICs) track the types/shapes seen at each call site or
property access:
| IC State | Shapes Seen | Speed |
|---|
| Monomorphic | 1 | Fastest — single direct check |
| Polymorphic | 2–4 | Fast — linear search through cases |
| Megamorphic | 5+ | Slow — hash-table lookup, no inlining |
Once an IC goes megamorphic it does NOT recover (until the function is
re-compiled). Megamorphic ICs also prevent Turbofan from inlining the
function.
Keep Hot Call Sites Monomorphic
function processChunk(chunk: Uint8Array): void {
}
function processChunk(chunk: Uint8Array | Buffer | string): void {
}
Practical strategies:
- Normalize inputs at the boundary (e.g. convert
Buffer → Uint8Array
once) and keep internal functions monomorphic.
- Avoid passing both
null and undefined for the same parameter — pick one
sentinel value.
- When a function must handle multiple types, split into separate specialized
functions and dispatch once at the entry point:
function handleStream(stream: ReadableStream | Readable) {
if (stream instanceof ReadableStream) {
return handleWebStream(stream)
}
return handleNodeStream(stream)
}
This is the pattern used in stream-ops.ts and throughout the stream-utils
code (Node.js vs Web stream split via compile-time switcher).
Closure and Allocation Pressure
Every closure captures its enclosing scope. Creating closures in hot loops
or per-request paths generates GC pressure and can prevent escape analysis.
Hoist Closures Out of Hot Paths
function handleRequest(req) {
stream.on('data', (chunk) => processChunk(chunk, req.id))
}
const requestIdByStream = new WeakMap()
function onData(chunk) {
const id = requestIdByStream.get(this)
if (id !== undefined) processChunk(chunk, id)
}
function processChunk(chunk, id) {
}
function handleRequest(req) {
requestIdByStream.set(stream, req.id)
stream.on('data', onData)
}
class StreamProcessor {
id: string
constructor(id: string) {
this.id = id
}
handleChunk(chunk: Uint8Array) {
processChunk(chunk, this.id)
}
}
Avoid Allocations in Tight Loops
for (const item of items) {
doSomething({ key: item.key, value: item.value })
}
const scratch = { key: '', value: '' }
for (const item of items) {
scratch.key = item.key
scratch.value = item.value
doSomething(scratch)
}
Real Codebase Example
node-stream-helpers.ts hoists encoder, BUFFER_TAGS, and tag constants to
module scope to avoid re-creating them on every request. The bufferIndexOf
helper uses Buffer.indexOf (C++ native) instead of a per-call JS loop,
eliminating per-chunk allocation.
Array Optimizations
V8 tracks array "element kinds" — an internal type tag that determines how
elements are stored in memory:
| Element Kind | Description | Speed |
|---|
PACKED_SMI | Small integers only, no holes | Fastest |
PACKED_DOUBLE | Numbers only, no holes | Fast |
PACKED_ELEMENTS | Mixed/objects, no holes | Moderate |
HOLEY_* | Any of above with holes | Slower (extra bounds check) |
Transitions are one-way — once an array becomes HOLEY or PACKED_ELEMENTS,
it never goes back.
Rules
- Pre-allocate arrays with known size:
new Array(n) creates a holey array.
Prefer [] and push(), or use Array.from({ length: n }, initFn).
- Don't create holes:
arr[100] = x on an empty array creates 100 holes.
- Don't mix types:
[1, 'two', {}] immediately becomes PACKED_ELEMENTS.
- Prefer typed arrays only when you need binary interop/contiguous memory or
have profiling evidence that they help. For small/short-lived collections,
normal arrays can be faster and allocate less.
const indices: number[] = []
for (let i = 0; i < n; i++) {
indices.push(i)
}
const indices = new Array(n)
for (let i = 0; i < n; i++) {
indices[i] = i
}
Real Codebase Example
accumulateStreamChunks in app-render.tsx uses const staticChunks: Array<Uint8Array> = [] with push() — keeping a packed array of a single type
throughout its lifetime.
Function Optimization and Deopts
Hot-Path Deopt Footguns
arguments object: using arguments in non-trivial ways (e.g.
arguments[i] with variable i, leaking arguments). Use rest params
instead.
- Type instability at one call site: same operation sees both numbers and
strings (or many object shapes) and becomes polymorphic/megamorphic.
eval / with: prevents optimization entirely.
- Highly dynamic object iteration: avoid
for...in on hot objects; prefer
Object.keys() / Object.entries() when possible.
Favor Predictable Control Flow
function getStatus(code: number): string {
if (code === 200) return 'ok'
if (code === 404) return 'not found'
return 'error'
}
function getStatus(code: number): string | null | undefined {
if (code === 200) return 'ok'
if (code === 404) return null
}
Watch Shape Diversity in switch Dispatch
function render(node) {
switch (node.type) {
case 'div':
return { tag: 'div', children: node.children }
case 'span':
return { tag: 'span', text: node.text }
case 'img':
return { src: node.src, alt: node.alt }
}
}
This pattern is not always bad. Often the main pressure is at the shared
dispatch site (node.type), while properties used only in one branch stay
monomorphic within that branch. Reach for normalization/splitting only when
profiles show this site is hot and polymorphic.
String Operations
- String concatenation in loops is usually fine in modern V8 (ropes make
many concatenations cheap). For binary data, use
Buffer.concat().
- Template literals vs concatenation: equivalent performance in modern V8,
but template literals are clearer.
string.indexOf() > regex for simple substring checks.
- Reuse RegExp objects: don't create a
new RegExp() inside a hot
function — hoist it to module scope.
const ROUTE_PATTERN = /^\/api\//
function isApiRoute(path: string): boolean {
return ROUTE_PATTERN.test(path)
}
function isApiRoute(path: string): boolean {
return /^\/api\//.test(path)
}
Map and Set vs Plain Objects
Map is faster than plain objects for frequent additions/deletions
(avoids hidden class transitions and dictionary mode).
Set is faster than obj[key] = true for membership checks with
dynamic keys.
- For static lookups (known keys at module load), plain objects or
Object.freeze({...}) are fine — V8 optimizes them as constant.
- Never use an object as a map if keys come from user input (prototype
pollution risk + megamorphic shapes).
Profiling and Verification
V8 Flags for Diagnosing JIT Issues
node --trace-opt server.js 2>&1 | grep "my-function-name"
node --trace-deopt server.js 2>&1 | grep "my-function-name"
node --trace-opt --trace-deopt server.js 2>&1 | tee /tmp/v8-trace.log
node --trace-ic server.js 2>&1 | tee /tmp/ic-trace.log
node --print-opt-code --code-comments server.js
Targeted Profiling in Next.js
node --cpu-prof --cpu-prof-dir=/tmp/profiles \
node_modules/.bin/next build
node --cpu-prof --cpu-prof-dir=/tmp/profiles \
node_modules/.bin/next start &
node --trace-deopt $(which jest) --runInBand test/path/to/test.ts \
2>&1 | grep -i "deopt" | head -50
Using % Natives (Development/Testing Only)
With --allow-natives-syntax:
function hotFunction(x) {
return x + 1
}
%PrepareFunctionForOptimization(hotFunction)
hotFunction(1)
hotFunction(2) % OptimizeFunctionOnNextCall(hotFunction)
hotFunction(3)
console.log(%GetOptimizationStatus(hotFunction))
Checklist for Hot Path Code Reviews
Related Skills
$dce-edge — DCE-safe require patterns (compile-time dead code)
$runtime-debug — runtime bundle debugging and profiling workflow