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:
| Engine | Used By |
|---|---|
| V8 | Chrome, Edge, Node.js, Deno |
| SpiderMonkey | Firefox |
| JavaScriptCore (Nitro) | Safari, Bun |
| Hermes | React 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):
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:
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:
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:
// โ 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.
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:
// โ 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
// โ 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.
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.