How V8 Actually Runs Your JavaScript — And Why It Should Change How You Write It
How V8 Actually Runs Your JavaScript — And Why It Should Change How You Write It
A deep dive into V8's optimization pipeline, hidden classes, inline caches, and deoptimization — with actionable patterns for React and Node.js performance.
Table of Contents
- V8's Execution Pipeline
- Hidden Classes: The Shape of Your Objects
- Inline Caches: How V8 Learns Your Code
- Deoptimization: When Optimizations Fail
- Practical Patterns for React
- Practical Patterns for Node.js
- Profiling and Debugging V8
V8's Execution Pipeline
┌─────────────────────────────────────────────────────────────────────────────┐
│ V8 COMPILATION PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ JavaScript Source │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Parser │ ──► Abstract Syntax Tree (AST) │
│ └──────────┘ - Lazy parsing for unused functions │
│ │ - Eager parsing for immediately invoked │
│ ▼ │
│ ┌──────────────┐ │
│ │ Ignition │ ──► Bytecode │
│ │ (Interpreter)│ - Register-based bytecode │
│ └──────────────┘ - Collects type feedback via Inline Caches │
│ │ │
│ │ hot code path detected │
│ │ (invocation count threshold) │
│ ▼ │
│ ┌──────────────┐ │
│ │ TurboFan │ ──► Optimized Machine Code │
│ │ (Optimizing │ - Speculative optimizations │
│ │ Compiler) │ - Based on type feedback from ICs │
│ └──────────────┘ - Can be deoptimized if assumptions break │
│ │ │
│ │ deoptimization │
│ │ (type assumption violated) │
│ ▼ │
│ ┌──────────────┐ │
│ │ Back to │ ──► Performance cliff │
│ │ Ignition │ - Re-collect feedback │
│ └──────────────┘ - May re-optimize later │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Insight: Speculation-Based Optimization
V8 doesn't analyze your entire program statically. Instead, it:
- Runs code in the interpreter (Ignition), collecting runtime type information
- Speculates that future executions will match observed patterns
- Generates optimized machine code based on those speculations
- Deoptimizes (bails out) when speculation is wrong
This means consistent types and shapes are not just good practice—they're the foundation of V8's optimization strategy.
Hidden Classes: The Shape of Your Objects
What Hidden Classes Are
Every JavaScript object has an associated "hidden class" (internally called Map in V8, sometimes Shape in other engines). This is a descriptor of the object's structure—what properties it has, in what order, and their attributes.
┌─────────────────────────────────────────────────────────────────────────────┐
│ HIDDEN CLASS EXAMPLE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ const user = {}; Hidden Class: C0 (empty) │
│ user.name = "Alice"; Hidden Class: C1 { name: offset 0 } │
│ user.age = 30; Hidden Class: C2 { name: offset 0, age: offset 1}│
│ │
│ ┌────────────┐ │
│ │ C0 │ (empty object) │
│ └─────┬──────┘ │
│ │ add "name" │
│ ▼ │
│ ┌────────────┐ │
│ │ C1 │ { name: offset 0 } │
│ └─────┬──────┘ │
│ │ add "age" │
│ ▼ │
│ ┌────────────┐ │
│ │ C2 │ { name: offset 0, age: offset 1 } │
│ └────────────┘ │
│ │
│ Hidden classes form TRANSITION CHAINS. Same initialization order │
│ = same chain = same hidden class = fast property access. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Why Property Order Matters
// THESE CREATE DIFFERENT HIDDEN CLASSES
function createUserA() {
const user = {};
user.name = "Alice"; // Transition: C0 → C1
user.age = 30; // Transition: C1 → C2
return user;
}
function createUserB() {
const user = {};
user.age = 30; // Transition: C0 → C1' (different!)
user.name = "Alice"; // Transition: C1' → C2' (different!)
return user;
}
const a = createUserA(); // Hidden class: C2
const b = createUserB(); // Hidden class: C2' (NOT the same as C2)
// V8 sees these as completely different object shapes.
// Functions that operate on both will be polymorphic (slower).
Hidden Class Best Practices
// ❌ BAD: Conditional property initialization creates multiple hidden classes
function createUser(hasEmail) {
const user = {};
user.name = "Alice";
if (hasEmail) {
user.email = "alice@example.com";
}
user.age = 30;
return user;
}
// Objects from createUser(true) and createUser(false) have different shapes!
// ✅ GOOD: Always initialize all properties, use undefined for missing values
function createUser(hasEmail) {
return {
name: "Alice",
email: hasEmail ? "alice@example.com" : undefined,
age: 30,
};
}
// ✅ BETTER: Use object literal syntax for consistent shape
function createUser({ name, email, age }) {
return { name, email: email ?? undefined, age };
}
The "Dictionary Mode" Performance Cliff
When V8 can't track an object's shape efficiently, it falls back to "dictionary mode" (slow properties stored in a hash table):
// ❌ TRIGGERS DICTIONARY MODE
// 1. Too many properties (>~1000 in-object properties)
const bigObject = {};
for (let i = 0; i < 2000; i++) {
bigObject[`prop${i}`] = i; // Eventually switches to dictionary mode
}
// 2. Deleting properties
const user = { name: "Alice", age: 30, email: "a@b.com" };
delete user.email; // Object transitions to dictionary mode
// 3. Adding properties with non-standard attributes
Object.defineProperty(user, 'id', {
value: 123,
writable: false, // Non-default attribute triggers dictionary mode
});
// ✅ INSTEAD OF DELETE: Set to undefined
const user = { name: "Alice", age: 30, email: "a@b.com" };
user.email = undefined; // Keeps hidden class intact
Verifying Hidden Classes with d8 (V8 shell)
# Run with --allow-natives-syntax to access V8 internals
d8 --allow-natives-syntax script.js
function Point(x, y) {
this.x = x;
this.y = y;
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// Check if objects share the same hidden class
console.log(%HaveSameMap(p1, p2)); // true
const p3 = new Point(5, 6);
p3.z = 7; // Add extra property
console.log(%HaveSameMap(p1, p3)); // false
Inline Caches: How V8 Learns Your Code
The IC State Machine
Inline Caches (ICs) are the mechanism V8 uses to speed up property access and function calls by "remembering" what types it has seen.
┌─────────────────────────────────────────────────────────────────────────────┐
│ INLINE CACHE STATES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ UNINITIALIZED│ Never executed │
│ └───────┬───────┘ │
│ │ first execution │
│ ▼ │
│ ┌───────────────┐ │
│ │ MONOMORPHIC │ Seen exactly 1 shape │
│ │ │ ════════════════════ │
│ │ FASTEST │ Direct memory offset lookup │
│ │ │ Single type check + load │
│ └───────┬───────┘ │
│ │ second shape seen │
│ ▼ │
│ ┌───────────────┐ │
│ │ POLYMORPHIC │ Seen 2-4 shapes │
│ │ │ ════════════════════ │
│ │ SLOWER │ Linear search through known shapes │
│ │ │ Still optimizable by TurboFan │
│ └───────┬───────┘ │
│ │ 5th+ shape seen │
│ ▼ │
│ ┌───────────────┐ │
│ │ MEGAMORPHIC │ Too many shapes │
│ │ │ ════════════════════ │
│ │ SLOWEST │ Falls back to generic lookup │
│ │ │ Cannot be optimized by TurboFan │
│ └───────────────┘ │
│ │
│ GOAL: Keep hot paths MONOMORPHIC │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Monomorphic vs Polymorphic in Practice
// ✅ MONOMORPHIC: Same shape every time
function getX(point) {
return point.x; // IC sees same hidden class every call
}
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const points = [];
for (let i = 0; i < 10000; i++) {
points.push(new Point(i, i));
}
// All points have same hidden class → monomorphic IC → fast
points.forEach(p => getX(p));
// ❌ POLYMORPHIC: Multiple shapes
function getX(obj) {
return obj.x;
}
class Point2D { constructor(x, y) { this.x = x; this.y = y; } }
class Point3D { constructor(x, y, z) { this.x = x; this.y = y; this.z = z; } }
class Vector { constructor(x, y) { this.x = x; this.y = y; } }
const objects = [
new Point2D(1, 2),
new Point3D(1, 2, 3),
new Vector(1, 2),
{ x: 1, y: 2 }, // Object literal - yet another shape
];
// getX sees 4 different shapes → polymorphic IC → slower
objects.forEach(obj => getX(obj));
// ❌ MEGAMORPHIC: Too many shapes (>4)
function getValue(obj) {
return obj.value;
}
// Each object literal with different property sets = different shape
for (let i = 0; i < 100; i++) {
getValue({ value: i, [`prop${i}`]: true }); // 100 different shapes!
}
// getValue's IC is now megamorphic - will never be optimized
Function Call ICs
Function calls also have ICs. Calling the same function type keeps it monomorphic:
// ✅ MONOMORPHIC CALL SITE
class Dog {
speak() { return "woof"; }
}
function makeSpeak(animal) {
return animal.speak(); // IC for .speak() call
}
const dogs = Array.from({ length: 1000 }, () => new Dog());
dogs.forEach(makeSpeak); // Always Dog.speak → monomorphic
// ❌ POLYMORPHIC CALL SITE
class Dog { speak() { return "woof"; } }
class Cat { speak() { return "meow"; } }
class Bird { speak() { return "chirp"; } }
function makeSpeak(animal) {
return animal.speak(); // IC sees 3 different methods
}
const animals = [new Dog(), new Cat(), new Bird()];
animals.forEach(makeSpeak); // Polymorphic - 3 different speak() implementations
Deoptimization: When Optimizations Fail
Common Deoptimization Triggers
┌─────────────────────────────────────────────────────────────────────────────┐
│ DEOPTIMIZATION TRIGGERS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. TYPE CHANGE │
│ Function optimized for numbers, suddenly receives string │
│ │
│ 2. HIDDEN CLASS CHANGE │
│ Object shape differs from what was compiled for │
│ │
│ 3. OUT-OF-BOUNDS ARRAY ACCESS │
│ Accessing index beyond array length │
│ │
│ 4. HOLE IN ARRAY │
│ Sparse arrays or arrays with deleted elements │
│ │
│ 5. PROTOTYPE CHAIN MODIFICATION │
│ Changing prototype after optimization │
│ │
│ 6. ARGUMENTS OBJECT ESCAPE │
│ Passing `arguments` to another function │
│ │
│ 7. EVAL / WITH │
│ Dynamic scope prevents optimization │
│ │
│ 8. TRY-CATCH IN HOT PATH (legacy, less relevant in modern V8) │
│ Historically prevented optimization, now mostly fixed │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Type Instability
// ❌ TYPE INSTABILITY: Causes repeated deoptimization
function add(a, b) {
return a + b;
}
// First 10000 calls with numbers - V8 optimizes for numbers
for (let i = 0; i < 10000; i++) {
add(i, i);
}
// Suddenly call with strings - DEOPT!
add("hello", "world");
// V8 must now re-optimize for both numbers and strings
// Or give up and use a slower generic version
// ✅ TYPE STABLE: Separate functions for different types
function addNumbers(a, b) {
return a + b;
}
function concatenateStrings(a, b) {
return a + b;
}
// Each function stays monomorphic for its type
Array Deoptimization
// V8 tracks array "elements kinds" - optimized storage for different types
// ❌ MIXED ARRAYS: Prevent optimizations
const mixed = [1, "two", { three: 3 }]; // PACKED_ELEMENTS (generic, slow)
// ✅ HOMOGENEOUS ARRAYS: Enable optimizations
const numbers = [1, 2, 3, 4, 5]; // PACKED_SMI_ELEMENTS (fast)
const floats = [1.1, 2.2, 3.3]; // PACKED_DOUBLE_ELEMENTS (fast)
const strings = ["a", "b", "c"]; // PACKED_ELEMENTS but consistent
// Array elements kind transitions (one-way, can't go back!)
const arr = [1, 2, 3]; // PACKED_SMI_ELEMENTS
arr.push(4.5); // → PACKED_DOUBLE_ELEMENTS
arr.push("string"); // → PACKED_ELEMENTS (most generic)
// Once you add a non-integer, it never goes back to SMI
// Once you add a non-number, it never goes back to DOUBLE
// ❌ HOLEY ARRAYS: Another deoptimization path
const holey = [1, 2, , 4]; // HOLEY_SMI_ELEMENTS - has holes
const holey2 = new Array(100); // HOLEY_SMI_ELEMENTS - pre-allocated with holes
// ✅ AVOID HOLES: Initialize with fill() or Array.from()
const filled = new Array(100).fill(0); // PACKED_SMI_ELEMENTS
const mapped = Array.from({ length: 100 }, (_, i) => i); // PACKED_SMI_ELEMENTS
Checking Elements Kinds
// In d8 with --allow-natives-syntax
const arr = [1, 2, 3];
%DebugPrint(arr);
// Shows: elements: PACKED_SMI_ELEMENTS
arr.push(1.5);
%DebugPrint(arr);
// Shows: elements: PACKED_DOUBLE_ELEMENTS
Practical Patterns for React
Component Props and Hidden Classes
// ❌ BAD: Inconsistent prop shapes across renders
function UserCard({ user, showEmail, showAge, showAvatar }) {
// Each unique combination of boolean flags creates different render behavior
// But more importantly, the `user` object might have inconsistent shapes
return (
<div>
<span>{user.name}</span>
{showEmail && <span>{user.email}</span>}
{showAge && user.age && <span>{user.age}</span>} {/* user.age might not exist */}
</div>
);
}
// Different user objects passed in:
<UserCard user={{ name: "Alice", email: "a@b.com" }} />
<UserCard user={{ name: "Bob", age: 30 }} />
<UserCard user={{ name: "Carol", email: "c@d.com", age: 25, avatar: "url" }} />
// 3 different hidden classes → polymorphic access to user properties
// ✅ GOOD: Normalize data shapes at boundaries
interface User {
name: string;
email: string | null;
age: number | null;
avatar: string | null;
}
function normalizeUser(raw: Partial<User>): User {
return {
name: raw.name ?? "",
email: raw.email ?? null,
age: raw.age ?? null,
avatar: raw.avatar ?? null,
};
}
// Always pass normalized users to components
function UserCard({ user }: { user: User }) {
// user always has same shape → monomorphic property access
return (
<div>
<span>{user.name}</span>
{user.email && <span>{user.email}</span>}
{user.age !== null && <span>{user.age}</span>}
</div>
);
}
// Normalize at the data fetch boundary
const users = rawUsers.map(normalizeUser);
Hooks and Closure Allocation
// ❌ BAD: Creates new function on every render
function SearchBox({ onSearch }) {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
onSearch(e.target.value); // New closure every render
}}
/>
);
}
// ✅ GOOD: useCallback for stable references (but don't over-optimize)
function SearchBox({ onSearch }) {
const [query, setQuery] = useState("");
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
onSearch(value);
}, [onSearch]);
return <input value={query} onChange={handleChange} />;
}
// NOTE: useCallback has its own cost (dependency array comparison).
// Only use when:
// 1. Passing callback to memoized children (React.memo)
// 2. Callback is dependency of other hooks
// 3. Profiler shows excessive re-renders
Memoization and Object Identity
// ❌ BAD: New object reference every render breaks memoization
function Parent() {
const [count, setCount] = useState(0);
// This object is recreated every render
const config = { theme: "dark", locale: "en" };
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild config={config} /> {/* Re-renders every time! */}
</>
);
}
const MemoizedChild = React.memo(function Child({ config }) {
// config is always a "new" object, so memo comparison fails
return <div>{config.theme}</div>;
});
// ✅ GOOD: useMemo for stable object references
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: "dark", locale: "en" }), []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild config={config} /> {/* Skips re-render */}
</>
);
}
// ✅ ALTERNATIVE: Lift static objects outside component
const CONFIG = { theme: "dark", locale: "en" } as const;
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild config={CONFIG} />
</>
);
}
List Rendering and Key Stability
// ❌ BAD: Index as key causes unnecessary reconciliation work
function List({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li> // Reordering = all items re-render
))}
</ul>
);
}
// ✅ GOOD: Stable IDs maintain component identity
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li> // Reordering = minimal work
))}
</ul>
);
}
Avoiding Megamorphic Components
// ❌ BAD: Generic component that handles too many shapes
function GenericCard({ data }) {
// data can be User, Product, Order, etc. - megamorphic
return (
<div>
<h2>{data.title || data.name || data.id}</h2>
<p>{data.description || data.summary || data.details}</p>
</div>
);
}
// Used with wildly different shapes:
<GenericCard data={{ name: "Alice", summary: "..." }} />
<GenericCard data={{ title: "Product", description: "..." }} />
<GenericCard data={{ id: 123, details: "..." }} />
// ✅ GOOD: Type-specific components or normalized interface
interface CardData {
title: string;
description: string;
}
function toCardData(entity: User | Product | Order): CardData {
if ('name' in entity) {
return { title: entity.name, description: entity.summary ?? "" };
}
if ('title' in entity) {
return { title: entity.title, description: entity.description };
}
return { title: `#${entity.id}`, description: entity.details };
}
function Card({ data }: { data: CardData }) {
// Always same shape → monomorphic
return (
<div>
<h2>{data.title}</h2>
<p>{data.description}</p>
</div>
);
}
// Normalize before rendering
<Card data={toCardData(user)} />
Practical Patterns for Node.js
Hot Path Object Shapes
// ❌ BAD: Request handlers with inconsistent response shapes
app.get("/users/:id", async (req, res) => {
const user = await db.getUser(req.params.id);
if (!user) {
return res.json({ error: "Not found" }); // Shape 1
}
if (user.isDeleted) {
return res.json({ error: "User deleted", deletedAt: user.deletedAt }); // Shape 2
}
return res.json({ data: user }); // Shape 3
});
// res.json sees 3 different object shapes → polymorphic
// ✅ GOOD: Consistent response envelope
interface ApiResponse<T> {
success: boolean;
data: T | null;
error: string | null;
meta: Record<string, unknown>;
}
function success<T>(data: T, meta = {}): ApiResponse<T> {
return { success: true, data, error: null, meta };
}
function failure(error: string, meta = {}): ApiResponse<null> {
return { success: false, data: null, error, meta };
}
app.get("/users/:id", async (req, res) => {
const user = await db.getUser(req.params.id);
if (!user) {
return res.json(failure("Not found"));
}
if (user.isDeleted) {
return res.json(failure("User deleted", { deletedAt: user.deletedAt }));
}
return res.json(success(user));
});
// All responses have identical shape → monomorphic JSON serialization
Database Query Result Handling
// ❌ BAD: ORM returns objects with varying shapes based on query
const users = await prisma.user.findMany({
select: { id: true, name: true }, // Returns { id, name }
});
const usersWithEmail = await prisma.user.findMany({
select: { id: true, name: true, email: true }, // Returns { id, name, email }
});
// Processing functions see different shapes
function formatUser(user) {
return `${user.name} (${user.id})`; // Polymorphic
}
// ✅ GOOD: Explicit types and transformations
interface UserSummary {
id: string;
name: string;
email: string | null;
}
async function getUserSummaries(): Promise<UserSummary[]> {
const users = await prisma.user.findMany({
select: { id: true, name: true, email: true },
});
// Ensure consistent shape even if DB returns partial data
return users.map(u => ({
id: u.id,
name: u.name,
email: u.email ?? null,
}));
}
Worker Threads and Message Passing
// ❌ BAD: Sending objects with varying shapes between threads
worker.postMessage({ type: "PROCESS", data: someObject });
worker.postMessage({ type: "CANCEL", taskId: 123 });
worker.postMessage({ type: "STATUS" }); // No additional properties
// Worker receives messages with 3 different shapes
// ✅ GOOD: Discriminated unions with consistent structure
interface WorkerMessage {
type: "PROCESS" | "CANCEL" | "STATUS";
payload: ProcessPayload | CancelPayload | StatusPayload | null;
timestamp: number;
correlationId: string;
}
interface ProcessPayload { data: unknown }
interface CancelPayload { taskId: number }
interface StatusPayload {}
function createMessage(
type: WorkerMessage["type"],
payload: WorkerMessage["payload"] = null
): WorkerMessage {
return {
type,
payload,
timestamp: Date.now(),
correlationId: crypto.randomUUID(),
};
}
worker.postMessage(createMessage("PROCESS", { data: someObject }));
worker.postMessage(createMessage("CANCEL", { taskId: 123 }));
worker.postMessage(createMessage("STATUS"));
// All messages have identical top-level shape
Buffer and TypedArray Performance
// ❌ BAD: Creating new buffers in hot loops
function processChunks(chunks: Buffer[]) {
const results: Buffer[] = [];
for (const chunk of chunks) {
const processed = Buffer.alloc(chunk.length); // Allocation in hot path
for (let i = 0; i < chunk.length; i++) {
processed[i] = chunk[i] ^ 0xff;
}
results.push(processed);
}
return results;
}
// ✅ GOOD: Pre-allocate and reuse buffers
class ChunkProcessor {
private buffer: Buffer;
constructor(maxChunkSize: number) {
this.buffer = Buffer.alloc(maxChunkSize);
}
process(chunk: Buffer): Buffer {
// Reuse pre-allocated buffer for processing
for (let i = 0; i < chunk.length; i++) {
this.buffer[i] = chunk[i] ^ 0xff;
}
// Only allocate when returning result
return Buffer.from(this.buffer.subarray(0, chunk.length));
}
}
// For truly hot paths, consider returning views instead of copies
processInPlace(chunk: Buffer): Buffer {
for (let i = 0; i < chunk.length; i++) {
chunk[i] = chunk[i] ^ 0xff;
}
return chunk; // Mutate in place, no allocation
}
Profiling and Debugging V8
Node.js Built-in Profiling
# Generate V8 log with optimization info
node --trace-opt --trace-deopt app.js 2>&1 | head -100
# Output shows:
# [marking 0x... for optimization] - function being optimized
# [deoptimizing ...] - function being deoptimized (BAD)
# Profile with V8's internal profiler
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Look for:
# - "ticks" in unoptimized code
# - Frequent deoptimizations
# - Long GC pauses
Using d8 for Deep Inspection
# Install V8's debug shell
# (Usually requires building V8 or using jsvu)
jsvu --engines=v8-debug
v8-debug --allow-natives-syntax script.js
// script.js - Inspecting optimization status
function hot(x) {
return x * x + x;
}
// Warm up the function
for (let i = 0; i < 10000; i++) {
hot(i);
}
// Force optimization
%OptimizeFunctionOnNextCall(hot);
hot(1);
// Check status
console.log(%GetOptimizationStatus(hot));
// Returns bitmask - see V8 source for meaning
// 1 = optimized, 2 = always optimize, etc.
// Check if function can be optimized
console.log(%IsBeingInterpreted(hot)); // false if optimized
Chrome DevTools V8 Profiling
// In Chrome DevTools, use Performance tab
// Look for:
// 1. Long tasks in the main thread
// 2. Frequent minor/major GC
// 3. "Compile Script" and "Optimize Code" entries
// Enable "V8 Runtime Call Stats" in DevTools experiments
// Shows time spent in V8 internal operations
Identifying Megamorphic Call Sites
# Trace IC state transitions
node --trace-ic app.js 2>&1 | grep -E "(LoadIC|StoreIC|CallIC)"
# Look for lines showing state transitions:
# LoadIC in ~doSomething [monomorphic -> polymorphic]
# LoadIC in ~doSomething [polymorphic -> megamorphic] # BAD!
Memory and GC Profiling
// Expose GC for testing
// node --expose-gc app.js
global.gc(); // Force garbage collection
const before = process.memoryUsage();
// ... do work ...
global.gc();
const after = process.memoryUsage();
console.log({
heapUsedDelta: after.heapUsed - before.heapUsed,
externalDelta: after.external - before.external,
});
# Trace GC events
node --trace-gc app.js
# Output:
# [12345:0x...] 12 ms: Scavenge 2.1 (3.0) -> 1.8 (4.0) MB, 0.5 / 0.0 ms
# [12345:0x...] 234 ms: Mark-sweep 8.5 (10.0) -> 5.2 (10.0) MB, 12.3 / 0.0 ms
Summary: The Mental Model
┌─────────────────────────────────────────────────────────────────────────────┐
│ V8 OPTIMIZATION MENTAL MODEL │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ V8's optimizer is a PATTERN MATCHER. It observes your code, finds │
│ patterns, and generates fast code assuming those patterns continue. │
│ │
│ YOUR JOB: Be predictable. │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PREDICTABLE │ UNPREDICTABLE │ │
│ ├────────────────────────────────┼────────────────────────────────────┤ │
│ │ Consistent object shapes │ Conditional properties │ │
│ │ Homogeneous arrays │ Mixed-type arrays │ │
│ │ Stable types in functions │ Type-changing parameters │ │
│ │ Object literals (factory) │ Incremental property assignment │ │
│ │ undefined for missing props │ delete operator │ │
│ │ Monomorphic call sites │ Megamorphic call sites │ │
│ │ Pre-allocated arrays │ Sparse/holey arrays │ │
│ └────────────────────────────────┴────────────────────────────────────┘ │
│ │
│ WHEN IT MATTERS: │
│ - Hot loops (>10K iterations) │
│ - Frequently called functions │
│ - Real-time applications (games, video) │
│ - High-throughput servers │
│ │
│ WHEN IT DOESN'T: │
│ - One-time initialization code │
│ - UI event handlers (human-speed) │
│ - Code that runs once per request in low-traffic scenarios │
│ │
│ THE RULE: Write clean, typed code. Profile before optimizing. │
│ V8 is really good at optimizing normal JavaScript. │
│ Only apply these patterns when profiling shows a problem. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Takeaways
- Hidden classes are real - Property order and conditional initialization create different hidden classes
- Monomorphic is fast, megamorphic is slow - Keep hot paths seeing consistent types
- Arrays have elements kinds - Homogeneous arrays enable SIMD-like optimizations
- Deoptimization is expensive - Type instability causes repeated compile/deopt cycles
- Profile first - Most code doesn't need these optimizations; find actual bottlenecks before applying
What did you think?