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:

  1. Monomorphic (best case):
// Always one Hidden Class
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });
  1. Polymorphic (2-4 different classes):
getX({ x: 1, y: 2 });
getX({ x: 1, y: 2, z: 3 }); // Different Hidden Class
  1. 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

1

Avoid deoptimization

Don't change function argument types. Use TypeScript for type control.

2

Keep object shape

Initialize all properties in constructor. Don't add properties dynamically.

3

Monomorphic > Polymorphic > Megamorphic

Functions should work with objects of same structure (Hidden Class).

4

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.