Under the Hood of JavaScript: How V8, Ignition, and TurboFan Actually Execute Your Code
1. From Ten-Day Prototype to CPU Machine Code
I spent a stretch of my second year genuinely believing JavaScript was slow by design — that it was, at best, a scripting language living one interpreter loop away from being a glorified shell script. That belief was mostly leftover folklore from a much older internet, and it took actually reading through how V8 works to realize how wrong it was. Brendan Eich wrote the first version of JavaScript in ten days in 1995 for Netscape Navigator, and for a long time it deserved the "slow, interpreted" label people gave it. Then Chrome shipped in 2008 running on Lars Bak's V8 engine, and the ground shifted under the entire web. V8 doesn't read your JavaScript file top to bottom and execute it line by line the way people imagine an "interpreter" working. It compiles your code down to real machine instructions running on your CPU's actual hardware registers, and it does this dynamically, while your page is loading, without you ever noticing the machinery underneath.
Once you actually trace what happens between "the browser downloads a .js file" and "your function runs," it stops feeling like magic and starts feeling like a genuinely elegant piece of engineering — one built almost entirely around a single insight: most code either runs once or runs constantly, and those two situations deserve completely different treatment.
"V8 compiles your JavaScript down to real machine instructions running on your CPU's actual hardware registers, dynamically while your page loads."
2. Getting from Raw Text to Something the CPU Understands
V8 doesn't wait for your script to finish downloading before it starts working on it. Its streaming parser begins scanning and tokenizing UTF-8 text as bytes arrive over the network, building an Abstract Syntax Tree incrementally rather than waiting for the whole file. That AST then feeds into Ignition, V8's bytecode interpreter — register-based rather than stack-based, which sounds like a minor implementation detail until you realize it's a big part of why Ignition can produce compact bytecode and get your application running in milliseconds instead of waiting around for a heavier compilation pass.
Here's the part that actually surprised me the first time I read about it: Ignition isn't just executing your code, it's quietly taking notes while it does. Every time it runs a function, it records what types actually showed up, in a structure called the Feedback Vector. It's effectively building a runtime profile in real time — "this function has been called ten thousand times, and both arguments have always been numbers" — without you asking it to do any profiling at all.
// A monomorphic function — V8 can eventually collapse this to
// something close to a single CPU instruction
function add(a, b) {
return a + b;
}
// Ignition quietly logs the type feedback here: (number, number) -> number
for (let i = 0; i < 100000; i++) {
add(i, 2);
}3. Handing Hot Code to TurboFan
Once Ignition notices a function running often enough to matter — a "hot" function — it hands the bytecode and its accumulated Feedback Vector over to TurboFan, V8's optimizing JIT compiler. This is where V8 starts making bets. TurboFan looks at that feedback and essentially reasons: this function has taken two numbers a hundred thousand times in a row, so it's a safe assumption it'll take two numbers again. Based on that bet, it strips out the dynamic type checks JavaScript would normally need and compiles a specialized version straight into machine code — x86-64 or ARM64 assembly, running with none of the overhead a fully dynamic language usually carries.
The interesting part is what happens when that bet is wrong. Say that same add function suddenly gets called as add('hello', 5) after ten thousand calls with numbers. TurboFan doesn't crash or silently misbehave — it deoptimizes. The optimized machine code gets thrown away instantly, execution rewinds cleanly back to Ignition's bytecode interpreter, and your program keeps running correctly, just slower again until (and if) it re-stabilizes into a new optimized path. It's a genuinely elegant safety net: V8 gets to be aggressive about optimization precisely because it always has a safe fallback to retreat to.
4. Why the Shape of Your Objects Matters More Than You'd Expect
In a language like C++, an object's memory layout is fixed at compile time — point.x always lives at offset 0, point.y always at offset 8, full stop. JavaScript objects are conceptually just dynamic maps, where properties can be added, removed, or reordered at any point while the program is running. That flexibility is convenient to write, but it's brutal for performance if an engine has to actually treat every object like a real hash table on every access.
V8's answer is a system of hidden internal classes, generally called Maps (sometimes "Shapes" in the broader JS-engine literature). When you create { x: 1, y: 2 }, V8 assigns it a hidden class, and that class transitions as properties get added — think of it as a tree of shapes rather than a single fixed type. Two objects that add the exact same properties in the exact same order end up sharing the exact same hidden class, which means V8 can access their properties with something much closer to a fixed memory offset than a hash lookup. That's the mechanism behind inline caching: once V8 has seen a property access at a call site go through a specific hidden class a few times, it caches that path and skips the general-purpose lookup entirely.
The catch is that this optimization is fragile in a specific, learnable way. Delete a property, or add properties in an inconsistent order across similar objects, and V8 has to split the hidden class — degrading what was a fast inline-cache hit back down to a slow, general property lookup.
I used to think of this as a minor micro-optimization until I actually profiled a data-heavy app and watched property access time visibly drop just from constructing objects consistently instead of building them up ad hoc. It's one of those things that looks like superstition until you see the flame graph.
// Fast path — identical hidden class layout across instances (monomorphic)
class Point {
constructor(x, y) {
this.x = x; // shape transitions: base -> (+x)
this.y = y; // shape transitions: (+x) -> (+x, +y)
}
}
const p1 = new Point(10, 20);
const p2 = new Point(30, 40); // shares p1's hidden class exactly
// Slow path — ad hoc property assembly breaks hidden-class sharing
const p3 = {};
p3.x = 10;
p3.y = 20;
delete p3.x; // this specifically forces a hidden-class split — worth avoiding in hot code5. How V8 Cleans Up After Itself
Memory management runs on a garbage collector called Orinoco, built around what's usually called the generational hypothesis — the observation that in most real programs, the overwhelming majority of objects are created, used briefly, and discarded almost immediately, while a small minority stick around for the life of the program.
V8 splits the heap accordingly. New Space, the young generation, is a small buffer — typically somewhere in the low single-digit megabytes up to around 8MB — where freshly created objects land. It gets cleared constantly using a Cheney-style scavenge algorithm, and critically, this runs in parallel without freezing your page's main thread. Objects that survive several scavenge cycles get promoted into Old Space, the tenured generation, which is managed by a concurrent mark-sweep-compact collector instead — a heavier but far less frequent process, since long-lived objects don't need to be re-evaluated nearly as often as short-lived ones.
None of this is something you need to actively manage. But writing code that keeps object shapes predictable, and that doesn't hang onto long-lived references it doesn't need, plays directly into how both the optimizer and the garbage collector are designed to work — which is a big part of why some JavaScript feels instant and some quietly bogs an interface down without an obvious reason why.