V8 Architecture - from Code to Machine Instructions
V8 is a high-performance JavaScript engine by Google, used in Chrome and Node.js. Understanding its architecture is critical for writing optimized code.
V8 Pipeline: Three-Level Compilation
Parser → AST
function add(a, b) {
return a + b;
}
AST (Abstract Syntax Tree):
{
"type": "FunctionDeclaration",
"id": { "name": "add" },
"params": [
{ "name": "a" },
{ "name": "b" }
],
"body": {
"type": "ReturnStatement",
"argument": {
"type": "BinaryExpression",
"operator": "+",
"left": { "name": "a" },
"right": { "name": "b" }
}
}
}
Ignition Interpreter
Ignition converts AST to bytecode and starts execution:
// Bytecode for add(a, b)
Ldar a0 // Load argument 0 (a) into accumulator
Add a1, [0] // Add argument 1 (b)
Return // Return result
Why bytecode?
- Fast startup (no compilation needed)
- Memory efficient (more compact than AST)
- Easy to deoptimize back
Sparkplug - Baseline Compiler
Sparkplug (added in V8 9.1) compiles frequently called bytecode to unoptimized machine code:
Bytecode → Machine Code (1:1 mapping)
Advantages:
- Faster than interpreter (~2x)
- No profiling overhead
- Intermediate level before TurboFan
TurboFan - Optimizing Compiler
TurboFan creates highly optimized machine code based on feedback:
function add(a, b) {
return a + b;
}
// After 1000+ calls with numbers
add(1, 2); // V8 notices: always numbers!
add(5, 10);
add(100, 200);
// TurboFan optimizes: assuming numbers
// Generates machine code for numbers directly
TurboFan Optimizations:
- Type specialization
- Inline caching
- Function inlining
- Loop unrolling
- Dead code elimination
Optimization and Deoptimization
When Does Optimization Occur?
function calculate(x) {
return x * 2;
}
// Call 1-100: Ignition (bytecode)
for (let i = 0; i < 100; i++) calculate(i);
// Call 100+: Sparkplug (baseline)
// V8: "This function is hot, compile to baseline"
// Call 1000+: TurboFan (optimized)
// V8: "Always numbers! Optimize for numbers"
Deoptimization (Optimization Rollback)
function calculate(x) {
return x * 2;
}
// V8 optimized for numbers
for (let i = 0; i < 10000; i++) {
calculate(i); // Numbers - optimized code
}
// Unexpected type!
calculate("hello"); // String - DEOPT!
// V8 rolls back to bytecode and collects feedback again
Deopt Cost:
- Rollback to bytecode (slow)
- Loss of optimized code
- Re-collecting statistics
Hidden Classes (Shapes/Maps)
V8 optimizes property access through Hidden Classes:
class Point {
constructor(x, y) {
this.x = x; // Hidden Class C0 → C1
this.y = y; // Hidden Class C1 → C2
}
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// p1 and p2 have same Hidden Class C2
Hidden Class stores:
- List of properties and their offsets
- Value types
- Transitions (changes when adding properties)
Performance Killers
// Bad: different Hidden Classes
const p1 = { x: 1, y: 2 };
const p2 = { y: 2, x: 1 }; // Different order!
// Bad: dynamic properties
const p3 = { x: 1, y: 2 };
p3.z = 3; // New Hidden Class!
// Good: same structure
const p4 = { x: 1, y: 2 };
const p5 = { x: 3, y: 4 }; // Same Hidden Class
Inline Caching (IC)
Inline Cache speeds up property access by caching their location:
function getX(point) {
return point.x;
}
// First call
getX({ x: 1, y: 2 });
// V8: "x is at offset 0 for Hidden Class C2"
// Subsequent calls
getX({ x: 3, y: 4 });
// V8: "Same Hidden Class! Use cache - offset 0"
IC Types:
- Monomorphic (best case):
// Always one Hidden Class
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });
- Polymorphic (2-4 different classes):
getX({ x: 1, y: 2 });
getX({ x: 1, y: 2, z: 3 }); // Different Hidden Class
- Megamorphic (5+ classes - slow!):
// Bad: too many different structures
for (let i = 0; i < 10; i++) {
getX({ x: 1, [i]: i }); // New Hidden Class every time!
}
Memory Management: Heap Organization
Generational Garbage Collection
Idea: Most objects die young.
Scavenge GC (Young Generation):
// Create temporary objects
function process() {
const temp = { data: new Array(1000) }; // Dies immediately
return temp.data.length;
}
// temp destroyed by Scavenge GC (~1-2ms)
Mark-Sweep-Compact (Old Generation):
// Long-lived objects
const cache = new Map(); // Survives Scavenge → Old Gen
cache.set('key', largeData);
// Removed by Major GC (~50-100ms)
Performance Best Practices
Avoid deoptimization
Don't change function argument types. Use TypeScript for type control.
Keep object shape
Initialize all properties in constructor. Don't add properties dynamically.
Monomorphic > Polymorphic > Megamorphic
Functions should work with objects of same structure (Hidden Class).
Avoid delete
delete obj.prop creates new Hidden Class. Use obj.prop = undefined.
Optimization Examples
// Bad: different types
function add(a, b) {
return a + b;
}
add(1, 2); // Numbers
add("a", "b"); // Strings - DEOPT!
// Good: one type
function addNumbers(a, b) {
return a + b;
}
function addStrings(a, b) {
return a + b;
}
// Bad: dynamic properties
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const p = new Point(1, 2);
p.z = 3; // New Hidden Class!
// Good: all properties in constructor
class Point3D {
constructor(x, y, z = 0) {
this.x = x;
this.y = y;
this.z = z; // Always same structure
}
}
Analysis Tools
Chrome DevTools
// Check function optimization
%OptimizeFunctionOnNextCall(myFunction); // --allow-natives-syntax
myFunction();
%GetOptimizationStatus(myFunction);
Node.js
# Run with V8 flags
node --trace-opt --trace-deopt app.js
# Output:
# [optimizing: myFunction / 0x...]
# [bailout: myFunction - type mismatch]
Summary:
V8 uses multi-tier compilation (Ignition → Sparkplug → TurboFan) for balance between startup speed and performance. Understanding Hidden Classes, Inline Caching, and deoptimization conditions helps write code that V8 can efficiently optimize.