How V8 runs JavaScript: parse, bytecode, JIT
JavaScript is dynamically typed and starts life as text, yet it runs at speeds that ought to be impossible for an interpreted language — and the reason is that a modern engine like V8 is not an interpreter but a pipeline that starts by interpreting cheap bytecode and then compiles only the hot parts to optimized machine code. This lesson builds that tiered pipeline as the frame the whole module hangs on.
How V8 runs JavaScript: parse, bytecode, JIT
JavaScript should, by rights, be slow. It's handed to the browser as text, it's dynamically typed so a variable can hold a number one moment and an object the next, and objects can sprout new properties at runtime. None of that is friendly to the kind of ahead-of-time compilation that makes C fast. And yet modern JavaScript runs within a small multiple of native code. The gap between "should be slow" and "is fast" is filled by a surprisingly elaborate machine — the JavaScript engine (V8 in Chrome and Node, with SpiderMonkey and JavaScriptCore as close cousins) — and understanding its shape turns a lot of folklore ("don't change object types," "monomorphic is faster") into things you can derive.
The frame for the whole module: a modern engine does not simply interpret your code, nor does it simply compile it. It does both, in tiers — it starts by cheaply interpreting bytecode so the page starts fast, watches which code actually runs hot, and then invests in compiling just that hot code to optimized machine code. Everything else is detail on that sentence.
The naive options, and why neither wins alone
Imagine you're building an engine. You have two obvious strategies for turning source into running code.
Option A: compile everything to optimized machine code up front, like a C compiler. This gives the fastest possible execution — but for JavaScript it's a terrible default. Compiling is slow, and a web page ships megabytes of JS of which most runs once or never (initialization, event handlers that may never fire). Spending expensive compilation on code that runs once means the page sits blank while you optimize functions nobody calls. Startup would be dreadful.
Option B: interpret the source directly, walking it and executing as you go. This starts instantly and uses little memory — but every operation pays interpretation overhead on every execution, so genuinely hot code (a loop running a million times) crawls.
The insight that every modern engine is built on: these aren't mutually exclusive, and the right choice is different for different code. The vast majority of your code runs rarely — interpret that, cheaply. A tiny fraction runs constantly and dominates the runtime — compile that, expensively, because the investment pays off across millions of executions. You just need to figure out which code is which, at runtime. That realization is the entire architecture.
The pipeline V8 actually uses
So V8 is a pipeline with several stages, each handing off to the next:
Parse → AST. The source text is parsed into an Abstract Syntax Tree, a structured representation of the program. Crucially this is done lazily: V8 does a fast pre-parse to find function boundaries but skips fully parsing a function's body until that function is actually called — because, again, most functions on a page never run. (The parsing lesson takes this apart.)
Ignition → bytecode. The AST is compiled to bytecode — a compact, register-based instruction set for V8's own virtual machine — and V8's interpreter, called Ignition, executes that bytecode. This is the baseline tier: quick to produce, low memory, and it's where all code starts running. Fast startup comes from here. (The bytecode lesson.)
Profile → detect hot code. As Ignition executes, it profiles — counting how often functions are called and loops iterate, and recording the types of values flowing through. A function that runs a lot is "hot." This profiling data is the fuel for the next stage.
TurboFan → optimized machine code. When a function is hot enough, V8 hands it to TurboFan, the optimizing compiler, which produces genuinely fast native machine code — using the profiled type information to make assumptions (this variable is always a number, this object always has this shape) that let it skip JavaScript's dynamic overhead. (The JIT lesson.)
Deoptimize → fall back. Those assumptions are speculative. If a function optimized on the belief that x is always a number is suddenly handed a string, the optimized code is invalid — so V8 deoptimizes: throws away the machine code and drops that function back to running in Ignition, where anything goes. This safety valve is what lets the engine bet aggressively without ever being wrong.
Why "just in time" is the exact right name
Compiling ahead of time (AOT) means before the program runs; V8's TurboFan compiles just in time — during execution, once it has seen how the code actually behaves. That timing is the whole trick and it's only possible because it happens late. An AOT compiler for JavaScript would have to be pessimistic: since any variable could hold any type, it must emit code that handles every case. A JIT compiler, having watched the code run, gets to be optimistic: it has evidence that x has been a number every one of the ten thousand times this ran, so it emits code specialized for numbers and guards it with a cheap check. Runtime information is a resource the AOT compiler simply doesn't have, and the JIT's entire advantage is spending it.
The tension that drives every optimization ahead
Step back and notice the enemy the whole pipeline is fighting: JavaScript's dynamism. A variable can hold anything. An object can gain or lose properties after creation. A function can be called with arguments of any type. Every one of these freedoms is something a naive compiler would have to handle with slow, general-purpose code that checks types at every step.
The engine's cleverness is a set of techniques for discovering the regularity hiding inside that dynamism — because real code, in practice, is far more regular than the language permits. The object you create in a loop almost always has the same shape every iteration; the function almost always gets numbers. Hidden classes (lesson 5) are how V8 discovers and exploits stable object shapes. Inline caches are how it makes repeated property accesses fast once it's seen the shape. Speculative optimization in TurboFan is how it bets on stable types. Even the value representation and the garbage collector are shaped by this pressure. Every remaining lesson is, in some way, "here is another way the engine turns JavaScript's chaotic potential into fast, specialized code by betting on the orderly reality."
And it connects directly to everything you already know about the runtime: this pipeline runs on the one main thread that the event loop drives. Parsing, compiling, and garbage-collecting all happen in tasks on that thread — which is why a big script can block the loop, and why GC pauses show up as jank. The engine and the event loop are two views of the same machine.
Where this goes next
The pipeline starts with turning text into a tree, and that stage has more consequence for real-world performance than its humble position suggests — parsing is a big chunk of a page's startup cost, and V8 goes to real lengths to do as little of it as possible, as late as possible. Parsing and the AST covers how source becomes a syntax tree, what "lazy parsing" skips and why, and how the way you structure your code changes how much the engine has to parse before it can run anything.
Go deeper
- V8 blog — Understanding V8's bytecode / Ignition — The interpreter tier this lesson introduces, from the team that built it.
- Mathias Bynens — JavaScript engine fundamentals: Shapes and Inline Caches — The clearest walk-through of the hidden-class/inline-cache machinery this module builds toward, applicable across engines.
- V8 blog — Launching Ignition and TurboFan — The two-tier interpreter+optimizer pipeline this lesson frames, described by V8 directly.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- Give the two naive strategies for running JavaScript (compile-everything vs interpret) and the concrete downside of each for a web page.
- State the tiered insight that resolves that tradeoff. What determines which code gets compiled to machine code?
- Name the four main stages of V8's pipeline (parse, Ignition, profile, TurboFan) and say what each produces or does.
- What is deoptimization, when does it happen, and why does it let the optimizer bet aggressively without ever being wrong?
- Why is 'just in time' compilation able to be more optimistic than an ahead-of-time compiler for JavaScript? What resource does it have that AOT doesn't?
- Contrast this with WebAssembly: why does WASM need no profiling, speculation, or deopt, and what single fact explains the difference?
- What underlying property of JavaScript is the whole optimization apparatus fighting, and what is the general trick (discovering regularity) the engine uses against it?