Skip to main content
๐Ÿ”งadvanced

How V8 Works

V8 is the JavaScript engine used by Chrome and Node.js. Understanding how it parses, compiles, and optimizes your code helps you write faster JavaScript.

What is a JavaScript Engine?

A JavaScript engine takes your source code and executes it. Major engines:

EngineUsed By
V8Chrome, Edge, Node.js, Deno
SpiderMonkeyFirefox
JavaScriptCore (Nitro)Safari, Bun
HermesReact Native

All modern engines follow similar architecture, but V8 is the most studied.

V8's Pipeline: Code to Execution

Source Code (text) โ†“ [Parser] โ†“ Abstract Syntax Tree (AST) โ†“ [Ignition - Interpreter] โ†“ Bytecode โ†“ [Profiler - watches hot functions] โ†“ [TurboFan - JIT Compiler] โ†“ Optimized Machine Code

Step 1: Parsing

The parser reads your source text and builds an Abstract Syntax Tree (AST):

hljs javascript
const x = 1 + 2;

Becomes roughly:

VariableDeclaration (const) โ””โ”€ VariableDeclarator (x) โ””โ”€ BinaryExpression (+) โ”œโ”€ NumericLiteral (1) โ””โ”€ NumericLiteral (2)

The parser has two phases:

  • Eager parsing โ€” parses code that will run immediately (top-level code)
  • Lazy parsing โ€” does a shallow parse of function bodies (avoids parsing unused functions upfront)

Step 2: Ignition โ€” The Interpreter

Ignition compiles the AST to bytecode โ€” a compact, platform-independent instruction set. Then it executes the bytecode directly.

Bytecode is much faster to generate than machine code, which is why startup time is fast. But bytecode is slower to execute than optimized machine code.

// Your code: function add(a, b) { return a + b; } // Rough bytecode: Ldar a Add b, [slot 0] Return

Step 3: TurboFan โ€” The JIT Compiler

While Ignition runs, the profiler watches for "hot" functions โ€” those called many times. When a function is hot, TurboFan compiles it to optimized native machine code using a technique called Just-In-Time (JIT) compilation.

This optimized code runs at near-native speed.

Speculative Optimization

TurboFan makes assumptions based on observed types. If add is always called with numbers:

hljs javascript
function add(a, b) { return a + b; }
add(1, 2);  // V8 notes: both args are numbers
add(3, 4);
add(5, 6);

TurboFan generates optimized code assuming a and b are always numbers (integer addition is faster than generic addition).

Deoptimization

If an assumption is violated, V8 deoptimizes โ€” throws away the optimized code and falls back to the interpreter:

hljs javascript
function add(a, b) { return a + b; }

add(1, 2);      // โœ“ number + number
add(3, 4);      // โœ“ number + number
add("hello", 5); // โœ— string + number โ€” deoptimization!

Deoptimization is expensive. Consistent types = faster code.

Hidden Classes

V8 tracks the "shape" of objects using hidden classes (similar to compiled class shapes). Objects with the same shape share a hidden class:

hljs javascript
// โœ“ Efficient โ€” same shape, same hidden class
function makePoint(x, y) {
  const p = {};
  p.x = x;  // shape: {x}
  p.y = y;  // shape: {x, y}
  return p;
}
const p1 = makePoint(1, 2); // hidden class C1
const p2 = makePoint(3, 4); // reuses hidden class C1

// โœ— Inefficient โ€” different property order creates different hidden classes
const a = {};
a.x = 1; a.y = 2; // hidden class: {x, y}

const b = {};
b.y = 1; b.x = 2; // different hidden class: {y, x}!

โœ…Performance tip: consistent object shapes

Always add properties in the same order. Initialize all properties in the constructor. Avoid adding/deleting properties dynamically. This lets V8 optimize property access with inline caches.

Inline Caching

V8 uses inline caches (ICs) to speed up property lookups. The first time a property is accessed, V8 looks up the hidden class. Subsequent accesses on objects with the same hidden class are almost free.

hljs javascript
function getX(point) {
  return point.x; // IC: "for hidden class C1, x is at offset 0"
}

// Fast (all same hidden class):
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });
getX({ x: 5, y: 6 });

eval and with โ€” JIT Killers

Certain constructs prevent optimization:

hljs javascript
// โœ— eval creates new scope dynamically โ€” V8 can't optimize the enclosing function
function bad(str) {
  eval(str); // V8 can't know what this will do
  return x + y;
}

// โœ— with statement โ€” prevents scope resolution optimization
with (obj) {
  console.log(name); // is 'name' from obj or outer scope?
}

Both prevent V8 from knowing the scope structure at compile time.

Practical Performance Tips

hljs javascript
// โœ“ Monomorphic functions โ€” single type per argument
function addNums(a, b) { return a + b; } // always numbers

// โœ— Polymorphic โ€” multiple types deoptimize
function add(a, b) { return a + b; } // sometimes string, sometimes number

// โœ“ Consistent object shapes
class Vector {
  constructor(x, y) {
    this.x = x; // always defined in constructor
    this.y = y;
  }
}

// โœ— Dynamic property addition
const v = new Vector(1, 2);
v.z = 3; // changes hidden class!

// โœ“ Avoid delete
delete obj.property; // changes hidden class!

V8's Garbage Collector

V8 uses a generational garbage collector:

  • Young generation (Nursery) โ€” small, collected frequently using a fast Scavenger
  • Old generation โ€” objects that survive young GC, collected with full GC (Mark-Compact)

Most objects die young (short-lived temporaries). Long-lived objects get promoted to old generation.

The GC runs concurrently and incrementally to minimize pauses.

โ–ถTry it yourself

Key Takeaways

  • V8 pipeline: Source โ†’ AST โ†’ Bytecode (Ignition) โ†’ Optimized Machine Code (TurboFan)
  • JIT optimization is speculative โ€” V8 assumes types based on what it's seen
  • Changing types on hot functions causes deoptimization (expensive)
  • Keep object shapes consistent โ€” add properties in the same order, always in constructors
  • Avoid eval, with, and dynamic property deletion on hot objects
  • V8's generational GC is highly optimized for short-lived objects

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’