Ignition Interpreter and JS Bytecode Tutorial

Ignition is V8's bytecode interpreter that delivers fast startup by executing compact instructions while collecting type feedback for TurboFan. Learn how it works.

6 min read

Ignition is V8's bytecode interpreter. It takes the abstract syntax tree produced by the parser, generates compact bytecode instructions, and executes them immediately. Unlike V8's previous approach of compiling all JavaScript to machine code upfront, Ignition starts fast and uses less memory.

Ignition was introduced in 2016 to replace Full-Codegen, V8's earlier baseline compiler. The shift was driven by mobile: Full-Codegen generated machine code for every function, consuming megabytes of memory on devices with tight constraints. Ignition's bytecode is far more compact while its interpreter runs nearly as fast.

How Ignition Fits in the V8 Pipeline

Ignition sits between the parser and the optimizing compiler. It is the first tier that actually executes your code.

V8 compilation tiers with Ignition

Ignition plays two roles. First, it executes bytecode immediately so the program starts quickly. Second, it collects type feedback: what types flow through each operation, which functions are called most often, and which property shapes appear repeatedly.

This feedback is what makes TurboFan's optimizations possible.

Register-Based Bytecode

Ignition uses register-based bytecode rather than stack-based bytecode. In register-based bytecode, operands refer to virtual registers that hold intermediate values. This produces fewer instructions for the same operation compared to a stack-based approach.

A simple addition like a + b might take one instruction: add the value in register r0 to register r1 and store the result in register r2. A stack-based bytecode would need separate push and pop instructions around the add.

Ignition's virtual registers are not real CPU registers. They are slots in a frame-local array that the interpreter manages. The interpreter maps these virtual registers to memory locations and accesses them as needed during execution.

The Ignition Instruction Set

Ignition has roughly 200 opcodes, each representing a specific JavaScript operation. Some common ones include LdaConstant to load a constant value, Add to add two values with JavaScript semantics, CallProperty to invoke a method, Star to store a value into a register, and Return to exit a function.

Each instruction is a few bytes. The opcode is one byte. Operands like register indices and constant pool references take a few more bytes.

A typical function compiles to dozens of bytes of bytecode, far smaller than the equivalent machine code.

The interpreter loop reads each instruction, decodes the opcode, fetches operands from the register file, performs the JavaScript operation (with all its type coercion and prototype chain semantics), and advances to the next instruction. This loop runs millions of times per second. For more on what bytecode looks like, see /javascript/javascript-bytecode-explained-complete-guide.

The Interpreter Dispatch Loop

The heart of Ignition is a hand-tuned dispatch loop written in assembly. A naive interpreter would use a switch statement over opcodes, but that introduces branch prediction misses. Ignition uses computed goto or threaded code techniques where each instruction handler jumps directly to the next handler.

The loop structure is roughly: read the next bytecode opcode, jump through a dispatch table to the handler for that opcode, execute the handler, and repeat. The dispatch table maps each opcode byte to the address of its handler code.

Hand-written assembly allows V8 engineers to control register allocation and instruction scheduling for the dispatch loop. This level of optimization makes interpreted bytecode competitive with simple compiled code.

Constant Pool and Register File

Ignition stores function-wide constants in a constant pool rather than embedding them in each instruction. String literals, number constants, and references to outer-scope variables all live in the constant pool. Instructions reference constants by index.

The register file holds local variables, temporary values, and the accumulator. Ignition uses an accumulator register for many operations: the result of an addition, the value loaded from a property, or the return value of a function call all pass through the accumulator.

This accumulator-based design is a middle ground between pure register-based and pure stack-based bytecode. It keeps instructions short while avoiding the push/pop overhead of stack machines.

How Ignition Collects Feedback

While interpreting bytecode, Ignition records what happens. For every property access like obj.x, it notes which hidden class the object had. For every binary operation like a + b, it notes what types were involved.

For every function call, it tracks how often the function is called.

This feedback is stored in feedback vectors associated with each function. A feedback vector is an array of slots, one per bytecode instruction position. Over time, each slot accumulates data about what types and shapes appeared at that position.

TurboFan reads these feedback vectors when optimizing. If a slot consistently shows that a parameter is a number, TurboFan generates numeric machine code. If a slot shows mixed types, TurboFan generates more general code or decides not to optimize at all.

For the full V8 compilation story, see /javascript/how-the-google-v8-engine-compiles-javascript.

Ignition and Deoptimization

When TurboFan's assumptions break, V8 deoptimizes back to Ignition. The optimized code is discarded. Execution falls back to interpreting the original bytecode.

Ignition bytecode serves as the deoptimization target. It is the canonical representation of what the function does. TurboFan's optimized code is a speculative version that assumes certain types and shapes.

When those assumptions fail, Ignition bytecode is the fallback that always produces the correct result.

This is why Ignition bytecode must handle every JavaScript edge case. The interpreter implements all the language semantics: prototype chain lookups, type coercion, getters and setters, Proxy traps, and the full this-binding rules.

Ignition vs Other Engine Interpreters

Every major JavaScript engine has an interpreter tier, and each makes its own tradeoff between interpreter speed and bytecode compactness.

EngineInterpreterDesign Priority
V8IgnitionCompact bytecode, since V8 targets mobile heavily
SpiderMonkeyIts own bytecode interpreter, predating IgnitionSlightly faster interpretation over compactness
JavaScriptCoreLLInt (Low-Level Interpreter)Direct threaded interpretation

The common thread is that all modern engines now use a multi-tier approach with an interpreter feeding an optimizing compiler. For more on engine architecture, see /javascript/what-is-a-javascript-engine-a-complete-guide.

Rune AI

Rune AI

Key Insights

  • Ignition is V8's bytecode interpreter, replacing the older Full-Codegen baseline compiler in 2016.
  • It generates register-based bytecode that is more compact and faster to interpret than walking the AST.
  • Ignition collects type feedback during interpretation that TurboFan uses for optimization.
  • The interpreter loop is hand-written assembly for maximum dispatch speed.
  • Ignition bytecode uses about 200 different opcodes covering all JavaScript operations.
RunePowered by Rune AI

Frequently Asked Questions

Why did V8 switch from Full-Codegen to Ignition?

Full-Codegen compiled all JavaScript to machine code upfront, which used too much memory on mobile devices. Ignition's bytecode is far more compact and its interpreter is fast enough that the memory savings are worth the slight interpretation overhead.

Can I see the bytecode Ignition generates?

Yes. Run Node.js with --print-bytecode to see Ignition bytecode for each function. In Chrome, use the --js-flags="--print-bytecode" flag when launching from the command line.

Conclusion

Ignition is the engine inside the engine. It generates compact bytecode, interprets it immediately for fast startup, and collects the type feedback that makes TurboFan's optimizations possible. Understanding Ignition connects the gap between your source code and the optimized machine code that V8 eventually produces.