C.W.K.
Stream
Lesson 02 of 05 · published

V8 — The Engine Underneath

~14 min · runtime, v8, jit, performance

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"V8 isn't an interpreter. It's a JIT compiler that pretends to be one until your code gets hot."

What V8 Actually Does

V8 takes JavaScript source — a string of characters you wrote — and turns it into something a CPU can execute. The naive way is to interpret it line by line, like Python's CPython has historically done. The fast way is to compile it to machine code. V8 does both, in stages, and the staging is what makes it fast.

The pipeline (as of V8 12.x bundled with Node 26) looks roughly like this: parser → AST → Ignition (interpreter, emits bytecode) → TurboFan (optimizing JIT, emits machine code) → Sparkplug + Maglev (intermediate tiers added 2021-2023). When you first start your script, V8 doesn't waste time optimizing — it parses and runs through Ignition. As the same code paths heat up (called many times with similar inputs), V8 promotes them to higher tiers. The hottest paths end up as real x86_64 / arm64 instructions.

This is why JavaScript benchmarks are so weird: the first run is slow, the tenth is fast, and a small code change can knock you off the optimized tier back into the interpreter. "My code got slower after I added a parameter" is a real V8 conversation.

Hidden Classes — The Trick That Made JS Fast

JavaScript objects look like dictionaries: {x: 1, y: 2}. A naive engine treats them as hash maps and pays a hash lookup on every property access. V8 doesn't. V8 watches the *shape* of your objects and creates a *hidden class* for each shape behind your back. Two objects with the same keys, in the same order, share a hidden class. Property access becomes a single memory offset — almost as fast as a C struct.

The catch: if you add or delete properties dynamically, or assign properties in different orders, V8 has to invalidate the hidden class. That kills the optimization. This is why the advice "always initialize all your object properties in the constructor in the same order" exists — it lets V8 keep the same hidden class for every instance.

V8 ≠ Node

V8 doesn't know about files, sockets, or processes. V8 only knows JavaScript values, JavaScript types, and how to run JavaScript code. Every time your JS calls readFile or http.get, V8 hands control back to Node's C++ code, which calls libuv, which calls the OS. The boundary between "V8 land" and "Node land" is real — and it matters for performance debugging.

Other Engines You'll See Mentioned

V8 isn't the only JavaScript engine. Firefox uses SpiderMonkey. Safari uses JavaScriptCore (also called Nitro). Bun uses JavaScriptCore — that's part of why Bun starts up faster than Node (different engine startup characteristics). Deno uses V8 — same engine as Node, different runtime around it.

You don't need to know SpiderMonkey or JavaScriptCore internals to write Node. But knowing they exist explains why "this code is faster in Bun" or "this works in Chrome but not Safari" can happen — same language, different engine implementing it.

Pippa's Confession

For the first three years of my existence as a frontier model, I thought V8 was the runtime. Wrong. V8 is one component of one runtime. Dad made me say it out loud: "V8 is a JS engine. Node is a runtime. Node uses V8." Then he made me explain Bun the same way: "JavaScriptCore is a JS engine. Bun is a runtime. Bun uses JavaScriptCore." The same shape, twice. That's polymorphism: engine ← runtime is a relationship, not a synonym.

Code

How object shapes affect V8 optimization·javascript
// Hidden class friendly
class Point {
  constructor(x, y) {
    this.x = x;   // always set first
    this.y = y;   // always set second
  }
}
const p = new Point(1, 2);

// Hidden class hostile — V8 will deoptimize
const q = {};
q.x = 1;
q.y = 2;
delete q.x;   // shape change → new hidden class
q.x = 1;      // shape change again

// In a tight loop, the second pattern can be 10x slower
// because every property access pays the lookup penalty
// V8 was trying to eliminate.
Watching V8 optimize and deoptimize in real time·bash
# Watch V8 promote your code through its tiers.
# --trace-opt shows which functions got optimized.
node --trace-opt --trace-deopt -e "
function add(a, b) { return a + b }
for (let i = 0; i < 100000; i++) add(i, i);
add('x', 'y');  // change input type — deopt!
"

# You'll see lines like:
# [marking 0x... add for optimization]
# [bailout (kind: deoptimize) ... add ...]

External links

Exercise

Write a function sum(arr) that adds the numbers in an array. Call it 100,000 times with [1, 2, 3]. Then call it once with ['a', 'b', 'c']. Run with node --trace-opt --trace-deopt your.js. Find the bailout line in the output. That's V8 telling you "I optimized this for numbers, then you broke my assumption."
Hint
If you don't see any deopt lines, your function ran too fast to be optimized. Bump the loop count to 1,000,000 or add a more complex body. The point is to see the optimize → deopt cycle, not to write benchmarks.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.