v8-jit

安装量: 388
排名: #8465

安装

npx skills add https://github.com/vercel/next.js --skill v8-jit
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
// GOOD — consistent shape, single hidden class transition chain
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
// always initialize, even defaults
}
}
// BAD — conditional property addition creates multiple hidden classes
class
RequestContext
{
constructor
(
url
,
method
,
headers
,
options
)
{
this
.
url
=
url
this
.
method
=
method
if
(
options
.
timing
)
{
this
.
startTime
=
performance
.
now
(
)
// shape fork!
}
if
(
options
.
cache
)
{
this
.
cached
=
false
// another shape fork!
}
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.
// GOOD — same key order, shares hidden class
const
a
=
{
type
:
'static'
,
value
:
1
}
const
b
=
{
type
:
'dynamic'
,
value
:
2
}
// BAD — different key order, different hidden classes
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
// GOOD — always called with the same argument shape
function
processChunk
(
chunk
:
Uint8Array
)
:
void
{
// chunk is always Uint8Array → monomorphic
}
// BAD — called with different types at the same call site
function
processChunk
(
chunk
:
Uint8Array
|
Buffer
|
string
)
:
void
{
// IC becomes polymorphic/megamorphic
}
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:
// Entry point dispatches once
function
handleStream
(
stream
:
ReadableStream
|
Readable
)
{
if
(
stream
instanceof
ReadableStream
)
{
return
handleWebStream
(
stream
)
// monomorphic call
}
return
handleNodeStream
(
stream
)
// monomorphic call
}
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
// BAD — closure allocated for every request
function
handleRequest
(
req
)
{
stream
.
on
(
'data'
,
(
chunk
)
=>
processChunk
(
chunk
,
req
.
id
)
)
}
// GOOD — shared listener, request context looked up by stream
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
)
}
// BEST — pre-allocate the callback as a method on a context object
class
StreamProcessor
{
id
:
string
constructor
(
id
:
string
)
{
this
.
id
=
id
}
handleChunk
(
chunk
:
Uint8Array
)
{
processChunk
(
chunk
,
this
.
id
)
}
}
Avoid Allocations in Tight Loops
// BAD — allocates a new object per iteration
for
(
const
item
of
items
)
{
doSomething
(
{
key
:
item
.
key
,
value
:
item
.
value
}
)
}
// GOOD — reuse a mutable scratch object
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.
// GOOD — packed SMI array
const
indices
:
number
[
]
=
[
]
for
(
let
i
=
0
;
i
<
n
;
i
++
)
{
indices
.
push
(
i
)
}
// BAD — holey from the start
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 = []
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
// GOOD — predictable: always returns same type
function
getStatus
(
code
:
number
)
:
string
{
if
(
code
===
200
)
return
'ok'
if
(
code
===
404
)
return
'not found'
return
'error'
}
// BAD — returns different types
function
getStatus
(
code
:
number
)
:
string
|
null
|
undefined
{
if
(
code
===
200
)
return
'ok'
if
(
code
===
404
)
return
null
// implicitly returns undefined
}
Watch Shape Diversity in
switch
Dispatch
// WATCH OUT — node.type IC can go megamorphic if many shapes hit one site
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
}
// Many distinct node layouts can make this dispatch site polymorphic
}
}
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. // GOOD — regex hoisted to module scope const ROUTE_PATTERN = / ^ \/ api \/ / function isApiRoute ( path : string ) : boolean { return ROUTE_PATTERN . test ( path ) } // BAD — regex recreated on every call function isApiRoute ( path : string ) : boolean { return / ^ \/ api \/ / . test ( path ) // V8 may or may not cache this } 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

Trace which functions get optimized

node --trace-opt server.js 2

&1 | grep "my-function-name"

Trace deoptimizations (critical for finding perf regressions)

node --trace-deopt server.js 2

&1 | grep "my-function-name"

Combined: see the full opt/deopt lifecycle

node --trace-opt --trace-deopt server.js 2

&1 | tee /tmp/v8-trace.log

Show IC state transitions (verbose)

node --trace-ic server.js 2

&1 | tee /tmp/ic-trace.log

Print optimized code (advanced)

node --print-opt-code --code-comments server.js Targeted Profiling in Next.js

Profile a production build

node --cpu-prof --cpu-prof-dir = /tmp/profiles \ node_modules/.bin/next build

Profile the server during a benchmark

node --cpu-prof --cpu-prof-dir = /tmp/profiles \ node_modules/.bin/next start &

... run benchmark ...

Analyze in Chrome DevTools: chrome://inspect → Open dedicated DevTools

Quick trace-deopt check on a specific test

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 } // Force optimization % PrepareFunctionForOptimization ( hotFunction ) hotFunction ( 1 ) hotFunction ( 2 ) % OptimizeFunctionOnNextCall ( hotFunction ) hotFunction ( 3 ) // Check optimization status // 1 = optimized, 2 = not optimized, 3 = always optimized, 6 = maglev console . log ( % GetOptimizationStatus ( hotFunction ) ) Checklist for Hot Path Code Reviews All object properties initialized in constructor/literal, same order No delete on hot objects No post-construction property additions on hot objects Functions receive consistent types (monomorphic call sites) Type dispatch happens at boundaries, not deep in hot loops No closures allocated inside tight loops Module-scope constants for regex, encoders, tag buffers Arrays are packed (no holes, no mixed types) Map / Set used for dynamic key collections No arguments object — use rest params try/catch at function boundary, not inside tight loops String building via array + join() or Buffer.concat() Return types are consistent (no string | null | undefined mixes)

返回排行榜