Bytecode and Ignition: the baseline interpreter
The AST that parsing produced still isn't something V8 can run directly — it first gets compiled to compact bytecode for V8's own virtual machine, and that bytecode is what the Ignition interpreter actually executes as every function's very first tier.
Bytecode and Ignition: the baseline interpreter
The parsing lesson ended with an AST — a tree shape that's easy to reason about but slow to execute directly, since walking tree nodes over and over means re-checking what kind of node you're looking at every single time. V8's next move is to compile that tree into bytecode: a flat, compact instruction set for a virtual machine V8 itself defines. This lesson covers what that bytecode looks like, how Ignition, V8's interpreter, executes it, and why starting every function here — instead of compiling straight to machine code — is the right call.
Bytecode: a small instruction set for V8's own VM
Bytecode is a low-level, linear sequence of instructions, much closer to real machine code than an AST is, but still abstract enough to be portable across the different CPU architectures V8 runs on (x64, ARM, and others). Compared to walking an AST, executing bytecode is faster because there's no tree traversal or node-type dispatch on every step — just a flat list of small instructions executed in order, plus jumps for control flow. It's also compact: a bytecode instruction is typically a byte or two, versus the pointer-heavy tree of AST nodes it was generated from. And critically, it's cacheable — V8 can save the compiled bytecode and skip parsing and compiling entirely on a repeat visit (more on this below).
V8's bytecode is register-based with a special accumulator register that most instructions read from or write to implicitly, similar in spirit to how a real CPU has a small set of fast local storage slots instead of an unbounded scratch space.
A tiny function, sketched as bytecode
Take the same function from the parsing lesson:
function add(a, b) {
return a + b;
}Conceptually — this is illustrative, not the literal output V8 produces — its bytecode looks something like:
// Ldar a ; Load register "a" into the accumulator
// Add b ; Add register "b" to the accumulator, result stays in accumulator
// Return ; Return the value in the accumulatorLdar ("load accumulator from register") pulls a's value into the accumulator. Add b adds the value in register b to whatever's already in the accumulator, leaving the sum in the accumulator. Return hands back the accumulator's current value. Notice there's no tree to walk here — just three flat instructions, executed top to bottom, each one a couple of bytes. Compare this to the AST from the parsing lesson: the same a + b that was a BinaryExpression node with two Identifier children is now three linear instructions operating on named registers and one shared accumulator.
Ignition: the interpreter, and the baseline tier
Ignition is V8's bytecode interpreter — the component that actually walks this instruction stream and executes it, instruction by instruction. Ignition is the tier where every function starts: the very first time any function runs, it runs as bytecode in Ignition, no exceptions. It's also the tier that deoptimized code falls back to — when TurboFan's optimized machine code turns out to be wrong about a type assumption it made, execution doesn't crash or restart from scratch, it drops back down to running the same bytecode in Ignition, safe and general.
Why interpret bytecode first, instead of compiling to machine code immediately
This is the same tiering logic from the first lesson, made concrete at this stage. Compiling straight to optimized machine code the instant a function is first seen would mean paying a real compilation cost — analyzing the code, allocating registers, emitting native instructions — for functions that might run once, or never again after this call. Since most functions on a real page fall into exactly that bucket, an interpreter is the right default: producing bytecode from an AST is cheap, bytecode is quick to start executing, and it uses far less memory than a machine-code translation of every function that happens to get called. The page gets interactive sooner, and the engine hasn't wasted compilation effort on code that doesn't deserve it. Only code that keeps running — loops that iterate thousands of times, functions called over and over — earns the more expensive investment of compiling to real machine code, which is exactly what the next lesson covers.
Bytecode also collects type feedback
Ignition doesn't just execute bytecode — while it runs, it also records type feedback: what kinds of values actually flowed through a given operation each time it ran (was this property access always on an object with the same shape, was this addition always two numbers). This is stored in structures called feedback vectors, attached to each function. This feedback is exactly the evidence the JIT lesson relies on: TurboFan doesn't guess which optimizations are safe, it reads the feedback Ignition already gathered from real executions and compiles machine code specialized for the types that actually showed up.
Bytecode caching
Because generating bytecode from an AST still costs time — building the tree, walking it, emitting instructions — V8 also supports bytecode caching: on a repeat visit to a site, instead of re-parsing and re-compiling from source text, V8 can load previously-compiled bytecode straight from a cache. This skips lexing, parsing, and bytecode generation for a returning visitor entirely, running straight to Ignition executing cached instructions. It's the same instinct as lazy parsing from the previous lesson, aimed at a different moment: don't redo expensive work that's already been done once, if you can avoid it.
Where this goes next
Bytecode and Ignition are the floor every function stands on — fast to produce, fast enough to run, and instrumented with exactly the feedback the next stage needs. The JIT and deoptimization picks up from here: how TurboFan uses that feedback to compile hot functions to genuinely fast machine code, the assumptions it bets on to do so, and what happens — a fall back to this same Ignition bytecode — when one of those bets turns out wrong.
Go deeper
- V8 blog — Understanding V8's bytecode / Ignition — The team that built Ignition explaining the register-and-accumulator design and why it replaced V8's earlier full-codegen approach.
- V8 blog — Launching Ignition and TurboFan — How the bytecode interpreter and the optimizing compiler fit together as one pipeline, described by V8 directly.
- WebAssembly spec — Instructions — The formal stack-machine execution model to compare directly against V8's register-based bytecode from this lesson.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- What is bytecode, and what two advantages does it have over executing straight from the AST?
- Explain V8's register-plus-accumulator bytecode model using the Ldar/Add/Return example. What does each instruction do?
- Contrast V8's register-based bytecode with WebAssembly's stack-based bytecode. What does each style optimize for?
- What is Ignition, and what does it mean that it's the 'baseline tier'? Name the two situations where code runs in Ignition.
- Why does V8 interpret bytecode first rather than compiling every function straight to machine code?
- What is a feedback vector, what does it record, and which later pipeline stage consumes it?
- What problem does bytecode caching solve, and what pipeline steps does it let a repeat visit skip?