JavaScript Bytecode Explained: Complete Guide

Bytecode is the intermediate language between your JavaScript source and machine code. Learn how engines use bytecode for fast startup and how it feeds into JIT compilation.

6 min read

Bytecode is the intermediate language that sits between your JavaScript source code and native machine instructions. When an engine compiles your code, it does not go straight from source to machine code. It first translates the source into bytecode: a compact set of instructions that are faster to execute than parsing the original text but more abstract than processor-specific machine code.

Every major JavaScript engine generates bytecode. V8 uses Ignition to produce and interpret bytecode. SpiderMonkey has its own bytecode format.

JavaScriptCore uses a bytecode-based interpreter as part of its multi-tier compilation. Bytecode is the universal middle layer of JavaScript execution.

Why Engines Use Bytecode

Direct interpretation of source code is slow. Every time the engine encounters a variable name, it must look up the string in a table. Every operator requires checking types and resolving what operation to perform.

Bytecode pre-computes these decisions.

Bytecode instructions are small and fixed-format. Instead of storing the string "x + y" and parsing it each time, the bytecode stores a single byte for the Add operation and register indices for where x and y live. The interpreter can dispatch instructions in a tight loop with minimal overhead.

Bytecode is also compact. It takes less memory than both the original source and fully compiled machine code. This matters on mobile devices and in memory-constrained environments.

What Bytecode Looks Like

Bytecode is not meant to be human-readable, but understanding its structure helps explain how engines work. Each instruction has an opcode (the operation to perform) and operands (what to perform it on).

Here is a simple JavaScript function and a conceptual view of what its bytecode might contain.

javascriptjavascript
function add(a, b) {
  return a + b;
}

A simplified bytecode representation for this function might include instructions like: load the value of parameter a into a register, load parameter b into another register, add the two register values, and return the result. Each instruction is a few bytes, and the entire function compiles to a dozen or so bytes of bytecode.

Real engines use register-based bytecode where instructions operate on virtual registers rather than a stack. Register-based bytecode produces fewer instructions for the same operation compared to stack-based bytecode, which matters for interpretation speed.

V8 Ignition Bytecode in Detail

V8's Ignition produces a specific bytecode format designed for fast interpretation. Ignition bytecode is register-based, meaning operands refer to virtual registers that hold values during execution.

Ignition bytecode instructions include LdaConstant (load a constant), Add (add two registers), Star (store a register), CallProperty (call a named method), and Return (return from the function). There are roughly 200 different opcodes covering all JavaScript operations.

Ignition's interpreter is a hand-written assembly dispatch loop. It reads each bytecode instruction, decodes the opcode, fetches operands from registers, performs the operation, and advances to the next instruction. This tight loop runs millions of times per second.

For more on V8's compilation approach, see /javascript/how-the-google-v8-engine-compiles-javascript.

From Source to Bytecode

The journey from source to bytecode has two main steps. First, the parser reads the source and builds an AST. Second, the bytecode generator walks the AST and emits bytecode instructions.

The bytecode generator makes decisions that affect performance. It allocates virtual registers for local variables. It decides which values to cache as constants.

For control flow, it emits jump instructions with relative offsets, similar to how a compiler emits branch instructions for if statements and loops.

The bytecode is stored alongside the function object. When the function is called, the engine finds its bytecode and hands it to the interpreter. If the function becomes hot, the optimizing compiler reads the same bytecode and uses it as the basis for generating optimized machine code.

How Bytecode Enables JIT Compilation

Bytecode is not just for interpretation. It is the input to the optimizing compiler. When TurboFan optimizes a function, it reads Ignition's bytecode and the type feedback collected during interpretation.

The bytecode gives TurboFan a structured representation of the function's logic. The type feedback tells TurboFan what types actually flowed through each operation. Together they let TurboFan generate specialized machine code that assumes those types will stay the same.

This is the key benefit of bytecode over going directly from AST to machine code. The AST captures the source structure but not the execution behavior. Bytecode captures both: the operations to perform and, through feedback, how they are actually used.

Bytecode and Debugging

Bytecode preserves enough information to support debugging. Each bytecode instruction can be mapped back to a source location. When you set a breakpoint, the engine pauses at the bytecode instruction that corresponds to that source line.

Source maps, commonly used with minified code, work at a higher level than bytecode. They map minified source positions to original source positions before bytecode generation. The engine never needs to understand source maps at the bytecode level.

Comparing Bytecode Across Engines

Each engine uses its own bytecode format. V8's Ignition bytecode is different from SpiderMonkey's bytecode and from JavaScriptCore's. They serve the same purpose but are not interchangeable.

The formats differ in design choices. Some engines use accumulator-based bytecode where one implicit register holds the current value. Others use fully register-based bytecode where every operand is an explicit register.

These choices affect interpreter speed, bytecode compactness, and how easily the optimizing compiler can consume the bytecode.

Despite the format differences, the concept is the same across engines. For a broader look at how engines work, see /javascript/what-is-a-javascript-engine-a-complete-guide.

Bytecode vs Machine Code

Bytecode is not the final form of your code. It sits between the source and the fully compiled result, and the two forms differ in portability and speed.

BytecodeMachine Code
PortabilityRuns on any processor architecture through the interpreterArchitecture-specific: x64, ARM, and others need separate versions
Produced byIgnition, right after parsingTurboFan, after a function is identified as hot
Startup costLow, interpretation begins immediatelyHigher, compilation takes time before it runs
Execution speedSlower than optimized machine codeFastest, tailored to observed types and shapes

TurboFan generates x64 instructions on an Intel processor and ARM instructions on a phone, while the same bytecode runs unchanged on both. This is why the two-tier approach works well: bytecode starts fast everywhere, and machine code takes over once a function proves it is worth optimizing.

Rune AI

Rune AI

Key Insights

  • Bytecode is a compact, low-level representation of JavaScript that engines can execute quickly.
  • V8's Ignition generates register-based bytecode from the AST for immediate interpretation.
  • Bytecode enables fast startup because interpretation begins without full compilation.
  • Optimizing compilers use bytecode as input for generating specialized machine code.
  • Bytecode instructions represent JavaScript operations like property access, arithmetic, and function calls.
RunePowered by Rune AI

Frequently Asked Questions

Can I see the bytecode V8 generates for my code?

Yes. Run Node.js with the --print-bytecode flag. For browser JavaScript, use Chrome's about:flags to enable bytecode printing. The output shows Ignition bytecode instructions for each function.

Is bytecode the same as WebAssembly?

No. Bytecode is an internal engine format for JavaScript execution. WebAssembly is a separate binary format designed for near-native performance. They serve different purposes.

Conclusion

Bytecode is the bridge between your JavaScript source and native machine code. It lets engines start executing immediately without waiting for full compilation. Interpreters like V8's Ignition run bytecode directly, while optimizing compilers like TurboFan use it as input for generating high-performance machine code.