cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Bytecode & interpreters

Learn why engines run bytecode first, how V8 Ignition reads it with an accumulator interpreter, and how other engines start executing.

By the end, you can
  • 01
    Explain the bytecode trade-offContrast AST walking, compact bytecode, and immediate machine code for startup, memory, and tiering.
  • 02
    Read a V8 listingUse --print-bytecode output to identify offsets, operands, registers, feedback slots, constants, and jumps.
  • 03
    Trace an interpreter loopStep through a small accumulator/register VM and connect handlers, dispatch, feedback, and flushing to real engines.

Bytecode in one sentence

After scanning, parsing, and scope analysis, a JavaScript engine needs something it can run quickly. Bytecode is that compact middle form: lower-level than an AST, higher-level than CPU machine code, and structured enough for an interpreter and later JIT tiers to share.

Definition

Bytecode is a dense instruction stream for an engine's virtual machine. An interpreter reads one bytecode at a time, updates engine state such as the accumulator, registers, stack, or program counter, and returns the same JavaScript result the language requires.

Real-life analogyRecipe card and one mixing bowl

A chef does not reread the whole cookbook for every stir. They follow a short recipe card, keep the current mixture in one bowl, and move ingredients in and out of labeled containers. Ignition's accumulator bytecode has the same feeling.

In real life: Recipe card
In JavaScript: The bytecode array: short instructions in order
In real life: Cook
In JavaScript: The interpreter loop and its bytecode handlers
In real life: One mixing bowl
In JavaScript: The accumulator, reused by many operations
In real life: Labeled containers
In JavaScript: Virtual registers such as a0, r0, and r1

Where the analogy stops: A cook understands rich ingredients and taste. Bytecode handlers are exact engine routines; they execute specified operations and collect metadata, not human judgment.

We will use V8's Ignition as the main concrete example, then compare SpiderMonkey's Baseline Interpreter and JavaScriptCore's LLInt. Keep the nearby lessons in mind: bytecode sits after lazy parsing and before startup snapshots and code caching, while hot code continues into tiered compilation.

Why engines use bytecode

STARTUP + MEMORY

Engines could walk the AST directly. That is easy to imagine, but syntax trees are built for grammar: many object nodes, many pointers, and many cases that do not map cleanly to the next executable operation. Rewalking those nodes is unfriendly to caches and keeps parser data alive longer than necessary.

Engines could also compile every function straight to machine code. That can be fast once it runs, but first-run code often executes once during startup. Compiling all of it spends CPU and memory before the engine knows what becomes hot. V8's 2016 Firing up the Ignition interpreter post described Ignition bytecode as roughly 25% to 50% the size of equivalent baseline machine code and first enabled it for low-RAM Android devices.

Execution forms engines can choose
FormWhat runsTrade-off
AST interpreterWalks tree nodes directlySimple to build, but pointer-heavy trees cost memory and repeated tree walking is cache-unfriendly.
Register bytecodeCompact opcodes plus virtual registersV8 Ignition and JSC-style bytecode keep locals in frame slots and use concise operands.
Stack bytecodeOpcodes push and pop an operand stackSpiderMonkey JSOp bytecode records stack uses/defs and keeps instructions compact.
Machine codeCPU instructions for one architectureFast when worth compiling, but larger and slower to produce during startup.
The JIT still matters

Bytecode is not the end of the pipeline. V8 now has Ignition, Sparkplug, Maglev, and TurboFan; SpiderMonkey and JSC have their own tiers. The important bytecode lesson is that the first tier starts compact, gathers feedback, and gives hotter tiers a stable input.

Ignition's accumulator machine

a0, r0, acc

Ignition is a register machine with a special implicit accumulator. "Register" here means a virtual slot in the interpreter frame, not necessarily a hardware register. Function parameters print as a0, a1, and so on. Locals and temporaries print as r0, r1, and so on. The accumulator is not printed as an operand on most lines because many bytecodes read or write it implicitly.

Pieces of an Ignition listing
NameHow to read it
a0, a1Function parameters. V8's parameter count includes the receiver, so one user parameter prints as Parameter count 2.
r0…rNVirtual registers in the interpreter frame for locals and temporaries.
AccumulatorImplicit input/output for many bytecodes, so operands stay short.
Constant poolStrings, scope info, shared function info, and other constants referenced by index operands.
[n] operandsFeedback slot indexes for inline-cache/type-feedback sites such as Add, property loads, calls, and comparisons.

That implicit accumulator keeps bytecodes compact. Ldar a0 means "load parameter a0 into the accumulator." Star0 means "store the accumulator into r0." Add r0, [1] means "add r0 to the accumulator and use feedback slot 1 for runtime feedback."

Common V8 12.4 bytecodes in this lesson
GroupExamples
Load/storeLdaZero, LdaSmi, Ldar, Star0, Star1, LdaImmutableCurrentContextSlot
Arithmetic and testsAdd, AddSmi, MulSmi, TestLessThan with feedback slots when V8 records runtime types.
Properties and callsGetNamedProperty, SetNamedProperty, CallProperty1, CallUndefinedReceiver2. Older posts may show names such as LdaNamedProperty; Node 22/V8 12.4 prints the names here.
Control flowJumpIfFalse, JumpLoop, Return use bytecode offsets, not source line numbers.
ClosuresCreateFunctionContext, StaCurrentContextSlot, CreateClosure, and context-slot loads connect to scope analysis.
Version note

The listings in this lesson were captured from Node 22.23.1, which embeds V8 12.4.254.21-node.56. Opcode names are not a web standard. Older posts may use older names, and future V8 versions can print different sequences.

Read --print-bytecode

REAL NODE OUTPUT

You can ask Node to print V8 bytecode for a named function. The lesson tests write the source below to a child Node process, delete the Node test environment variables, and compare each stored opcode sequence with the live output. The addresses are normalized to <addr>.

Command used by the testsShell
node --print-bytecode --print-bytecode-filter=loopSum bytecode-demo.js
Demo source for the V8 listingsJavaScript
function arithmetic(a, b) {  const total = a + b;  return total * 2;} function propertyCall(user) {  user.count = user.count + 1;  return user.label(user.count);} function loopSum(n) {  let total = 0;  let i = 1;  while (i < n) {    total = total + i;    i = i + 1;  }  return total;} function makeAdder(x) {  return function add(y) {    return x + y;  };} function callFree(fn) {  return fn(1, 2);} const user = { count: 1, label(value) { return "#" + value; } };console.log(arithmetic(2, 3));console.log(propertyCall(user));console.log(loopSum(5));console.log(makeAdder(4)(6));console.log(callFree((a, b) => a + b));

Line by line: a loop

The loop listing starts with metadata. Parameter count 2 means receiver plus one user parameter. Register count 2 reserves r0 and r1 for total and i. The number after @ is the bytecode offset. Jumps target offsets, not source lines.

Node 22.23.1 (V8 12.4): loopSum bytecodeV8 bytecode
[generated bytecode for function: loopSum (<addr> <SharedFunctionInfo loopSum>)]Bytecode length: 31Parameter count 2Register count 2Frame size 16  207 S> <addr> @    0 : 0c                LdaZero         <addr> @    1 : c9                Star0  220 S> <addr> @    2 : 0d 01             LdaSmi [1]         <addr> @    4 : c8                Star1  234 S> <addr> @    5 : 0b 03             Ldar a0  234 E> <addr> @    7 : 71 f8 00          TestLessThan r1, [0]         <addr> @   10 : 9e 12             JumpIfFalse [18] (<addr> @ 28)  245 S> <addr> @   12 : 0b f8             Ldar r1  259 E> <addr> @   14 : 3b f9 01          Add r0, [1]         <addr> @   17 : c9                Star0  268 S> <addr> @   18 : 0b f8             Ldar r1  274 E> <addr> @   20 : 47 01 02          AddSmi [1], [2]         <addr> @   23 : c8                Star1  225 E> <addr> @   24 : 8e 13 00 03       JumpLoop [19], [0], [3] (<addr> @ 5)  285 S> <addr> @   28 : 0b f9             Ldar r0  298 S> <addr> @   30 : ae                ReturnConstant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 27)<addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>

Read the loop body as a sequence of accumulator moves: load n, test whether i < n, jump out if false, load i, add total, store the accumulator back to r0, increment i, then JumpLoop back to offset 5.

Properties, calls, and closures

Property operations have current names in V8 12.4: GetNamedProperty and SetNamedProperty. A method call such as user.label(user.count) becomes a CallProperty1 with the function, receiver, one argument, and a feedback slot.

Node 22.23.1 (V8 12.4): property and method bytecodeV8 bytecode
[generated bytecode for function: propertyCall (<addr> <SharedFunctionInfo propertyCall>)]Bytecode length: 27Parameter count 2Register count 3Frame size 24  124 S> <addr> @    0 : 2f 03 00 01       GetNamedProperty a0, [0], [1]  130 E> <addr> @    4 : 47 01 00          AddSmi [1], [0]  117 E> <addr> @    7 : 35 03 00 03       SetNamedProperty a0, [0], [3]  149 S> <addr> @   11 : 2f 03 01 05       GetNamedProperty a0, [1], [5]         <addr> @   15 : c9                Star0  160 E> <addr> @   16 : 2f 03 00 01       GetNamedProperty a0, [0], [1]         <addr> @   20 : c7                Star2  149 E> <addr> @   21 : 61 f9 03 f7 07    CallProperty1 r0, a0, r2, [7]  167 S> <addr> @   26 : ae                ReturnConstant pool (size = 2)<addr>: [TrustedFixedArray] - map: <addr> <Map(TRUSTED_FIXED_ARRAY_TYPE)> - length: 2           0: <addr> <String[5]: #count>           1: <addr> <String[5]: #label>Handler Table (size = 0)Source Position Table (size = 18)<addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>

Closures connect this lesson back to scope analysis. The outer function creates a function context and stores x in a context slot. The inner function later loads that slot before adding its own parameter.

Node 22.23.1 (V8 12.4): makeAdder bytecodeV8 bytecode
[generated bytecode for function: makeAdder (<addr> <SharedFunctionInfo makeAdder>)]Bytecode length: 14Parameter count 2Register count 1Frame size 8  320 E> <addr> @    0 : 88 00 01          CreateFunctionContext [0], [1]         <addr> @    3 : 1a f9             PushContext r0         <addr> @    5 : 0b 03             Ldar a0         <addr> @    7 : 25 02             StaCurrentContextSlot [2]  328 S> <addr> @    9 : 85 01 00 02       CreateClosure [1], [0], #2  375 S> <addr> @   13 : ae                ReturnConstant pool (size = 2)<addr>: [TrustedFixedArray] - map: <addr> <Map(TRUSTED_FIXED_ARRAY_TYPE)> - length: 2           0: <addr> <ScopeInfo FUNCTION_SCOPE>           1: <addr> <SharedFunctionInfo add>Handler Table (size = 0)Source Position Table (size = 10)<addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>
Node 22.23.1 (V8 12.4): inner add bytecodeV8 bytecode
[generated bytecode for function: add (<addr> <SharedFunctionInfo add>)]Bytecode length: 9Parameter count 2Register count 1Frame size 8  357 S> <addr> @    0 : 17 02             LdaImmutableCurrentContextSlot [2]         <addr> @    2 : c9                Star0         <addr> @    3 : 0b 03             Ldar a0  366 E> <addr> @    5 : 3b f9 00          Add r0, [0]  370 S> <addr> @    8 : ae                ReturnConstant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 9)<addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>

A free function call has no method receiver, so V8 prints CallUndefinedReceiver2 for two arguments in this example.

Node 22.23.1 (V8 12.4): callFree bytecodeV8 bytecode
[generated bytecode for function: callFree (<addr> <SharedFunctionInfo callFree>)]Bytecode length: 12Parameter count 2Register count 3Frame size 24   26 S> <addr> @    0 : 0d 01             LdaSmi [1]         <addr> @    2 : c8                Star1         <addr> @    3 : 0d 02             LdaSmi [2]         <addr> @    5 : c7                Star2   33 E> <addr> @    6 : 66 03 f8 f7 00    CallUndefinedReceiver2 a0, r1, r2, [0]   42 S> <addr> @   11 : ae                ReturnConstant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 8)<addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>

Build a teaching interpreter

MODEL, NOT V8

The browser experiment below compiles a tiny JavaScript subset with acorn, lowers it to a toy accumulator/register bytecode, and interprets it with a dispatch table. It is a teaching model inspired by Ignition; it does not implement JavaScript semantics, hidden classes, inline caches, exceptions, or real V8 bytecode.

Toy accumulator bytecode: sum loop
Step 0 of 90Ready
Your turn: follow the blue line

This is a model inspired by Ignition, not Ignition itself. Step through a loop and watch pc, the accumulator, and virtual registers change.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const program = [  ["Star","r0"],  ["LdaSmi",1],  ["Star","r1"],  ["Ldar","r1"],  ["Star","r2"],  ["Ldar","a0"],  ["Star","r3"],  ["LdaSmi",1],  ["Add","r3"],  ["TestLessThan","r2"],  ["JumpIfFalse",23],  ["Ldar","r0"],  ["Star","r4"],  ["Ldar","r1"],  ["Add","r4"],  ["Star","r0"],  ["Ldar","r1"],  ["Star","r5"],  ["LdaSmi",1],  ["Add","r5"],  ["Star","r1"],  ["Jump",4],  ["Ldar","r0"],  ["Return"],]; function run(bytecode, a0) {  const registers = { a0: a0, r0: undefined, r1: undefined };  let pc = 0;  let acc;  const dispatch = {    LdaSmi(value) { acc = value; pc += 1; },    Ldar(register) { acc = registers[register]; pc += 1; },    Star(register) { registers[register] = acc; pc += 1; },    Add(register) { acc = registers[register] + acc; pc += 1; },    Mul(register) { acc = registers[register] * acc; pc += 1; },    TestLessThan(register) { acc = registers[register] < acc; pc += 1; },    JumpIfFalse(target) { pc = acc ? pc + 1 : target; },    Jump(target) { pc = target; },    Return() { pc = bytecode.length; },  };  while (pc < bytecode.length) {    const [op, operand] = bytecode[pc];    dispatch[op](operand);  }  return acc;} console.log(run(program, 4));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The loop shows why a program counter matters. JumpIfFalse leaves the loop when the accumulator holds false; Jump returns to the condition. Back and Reset replay recorded snapshots from the real toy interpreter run.

Toy accumulator bytecode: multiply then add
Step 0 of 10Ready
Your turn: follow the blue line

A shorter run shows how expression temporaries move through the accumulator before a return.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const program = [  ["Star","r1"],  ["LdaSmi",2],  ["Mul","r1"],  ["Star","r0"],  ["Ldar","r0"],  ["Star","r2"],  ["LdaSmi",1],  ["Add","r2"],  ["Return"],]; function run(bytecode, a0) {  const registers = { a0: a0, r0: undefined };  let pc = 0;  let acc;  const dispatch = {    LdaSmi(value) { acc = value; pc += 1; },    Ldar(register) { acc = registers[register]; pc += 1; },    Star(register) { registers[register] = acc; pc += 1; },    Add(register) { acc = registers[register] + acc; pc += 1; },    Mul(register) { acc = registers[register] * acc; pc += 1; },    TestLessThan(register) { acc = registers[register] < acc; pc += 1; },    JumpIfFalse(target) { pc = acc ? pc + 1 : target; },    Jump(target) { pc = target; },    Return() { pc = bytecode.length; },  };  while (pc < bytecode.length) {    const [op, operand] = bytecode[pc];    dispatch[op](operand);  }  return acc;} console.log(run(program, 5));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

A short expression makes the accumulator easier to see: load a value, store a temporary, load the next value, combine, then return. The real V8 listing uses more specialized bytecodes, but the mental model is the same: one current value plus named storage.

Compile a tiny function to toy bytecode
Toy bytecode outputToy bytecode
0000 LdaSmi 0
0001 Star r0
0002 LdaSmi 1
0003 Star r1
0004 Ldar r1
0005 Star r2
0006 Ldar a0
0007 Star r3
0008 LdaSmi 1
0009 Add r3
0010 TestLessThan r2
0011 JumpIfFalse 23
0012 Ldar r0
0013 Star r4
0014 Ldar r1
0015 Add r4
0016 Star r0
0017 Ldar r1
0018 Star r5
0019 LdaSmi 1
0020 Add r5
0021 Star r1
0022 Jump 4
0023 Ldar r0
0024 Return
Result and real V8 comparisonNode 22.23.1 / V8 12.4
toy result10
real listing shownloopSum(n)
Stored V8 listing for loopSum(n)V8 bytecode
[generated bytecode for function: loopSum (<addr> <SharedFunctionInfo loopSum>)]
Bytecode length: 31
Parameter count 2
Register count 2
Frame size 16
  207 S> <addr> @    0 : 0c                LdaZero
         <addr> @    1 : c9                Star0
  220 S> <addr> @    2 : 0d 01             LdaSmi [1]
         <addr> @    4 : c8                Star1
  234 S> <addr> @    5 : 0b 03             Ldar a0
  234 E> <addr> @    7 : 71 f8 00          TestLessThan r1, [0]
         <addr> @   10 : 9e 12             JumpIfFalse [18] (<addr> @ 28)
  245 S> <addr> @   12 : 0b f8             Ldar r1
  259 E> <addr> @   14 : 3b f9 01          Add r0, [1]
         <addr> @   17 : c9                Star0
  268 S> <addr> @   18 : 0b f8             Ldar r1
  274 E> <addr> @   20 : 47 01 02          AddSmi [1], [2]
         <addr> @   23 : c8                Star1
  225 E> <addr> @   24 : 8e 13 00 03       JumpLoop [19], [0], [3] (<addr> @ 5)
  285 S> <addr> @   28 : 0b f9             Ldar r0
  298 S> <addr> @   30 : ae                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 27)
<addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>
Try it yourself
Preset and input

The model compiled and interpreted the function in 90 bytecode steps. It returned 10 with a0=4, r0=10, r1=5, r2=5, r3=4, r4=6, r5=4.

This is a teaching VM inspired by Ignition's accumulator/register shape. It is not V8 and it never evaluates arbitrary source as JavaScript.

Handlers and dispatch

OPCODE TO HANDLER

A bytecode interpreter is not a magical loop that understands every instruction by name. Each bytecode has handler code. Dispatch reads the opcode at the current bytecode offset, jumps to the handler, and the handler updates state before dispatching the next bytecode.

Tiny dispatch table modelPop out in the code editor (opens in a new tab)JavaScript
const handlers = {  LdaSmi(state, value) {    state.acc = value;    state.pc += 1;  },  Add(state, register) {    state.acc = state.registers[register] + state.acc;    state.pc += 1;  },  JumpIfFalse(state, target) {    state.pc = state.acc ? state.pc + 1 : target;  },}; function dispatch(bytecode, state) {  const [op, operand] = bytecode[state.pc];  return handlers[op](state, operand);}

V8's Ignition documentation describes threaded dispatch: every handler is a standalone chunk of machine code, and the end of a handler fetches the next bytecode and jumps through a dispatch table. V8's original Ignition post says those handlers were generated with TurboFan's low-level backend rather than handwritten for every architecture; later V8 infrastructure such as the CodeStubAssembler uses TurboFan's backend for portable low-level builtins.

Size matters inside the bytecode stream too. Stores to low-numbered registers use short forms such as Star0, Star1, and Star2; Node 22's listings above show them directly. These short stores are part of the same compactness story as the accumulator.

Flushing and lazy feedback allocation

V8 LITE

Bytecode saves memory, but it still occupies heap space. V8's V8 Lite work identified bytecode and feedback data as important memory contributors on real pages. The result was not just a separate lite mode; several ideas moved into regular V8.

Memory optimizations around bytecode
MechanismWhat to remember
Bytecode flushingV8 ages bytecode across major GCs, resets age when a function runs, and can discard old unused bytecode with --flush-bytecode enabled by default.
Reparse on demandIf a flushed function runs again, V8 recompiles its bytecode from source or cached data before executing.
Lazy feedback allocationV8 delays FeedbackVector allocation until enough bytecode has executed; Node 22 exposes --lazy-feedback-allocation and an invocation threshold flag.
Source positionsV8 can also collect detailed source positions lazily for stack traces and debugging instead of storing everything upfront.

The flag names are visible in current Node: --flush-bytecode is enabled by default, --lazy-feedback-allocation is enabled by default, and --invocation-count-for-feedback-allocation controls one threshold. These are engine implementation details, not knobs application code should casually depend on.

SpiderMonkey and JavaScriptCore

SAME IDEA, DIFFERENT VM

V8's names are useful because Node is easy to run, but the web does not standardize bytecode. Firefox's SpiderMonkey and Safari's JavaScriptCore also execute bytecode first, with their own formats, metadata, and tiering decisions.

First execution tiers across major engines
EngineFirst tierBytecode shape
V8IgnitionRegister bytecode with an implicit accumulator; Sparkplug and optimizing tiers consume the bytecode/feedback path.
SpiderMonkeyBaseline InterpreterJSOp bytecode is stack-based: opcode metadata declares stack uses and definitions; Baseline Interpreter interprets one opcode at a time and attaches ICs.
JavaScriptCoreLLIntJSC bytecode uses explicit operands such as loc and arg; the Low Level Interpreter is written in offlineasm and starts execution before JIT tiers.

Firefox source docs describe SpiderMonkey's Baseline Interpreter as a hybrid interpreter/JIT: it interprets bytecode one opcode at a time while attaching Inline Caches. SpiderMonkey's opcode definitions describe stack uses and definitions for bytecode instructions. WebKit's JSC bytecode post shows bytecode with operands such as loc7 and arg1, and WebKit docs describe the LLInt as the Low Level Interpreter written in offlineasm.

When this helps you

REAL WORK

You do not need bytecode for every performance investigation. Start with the Performance panel, real-user metrics, bundle analysis, and the call stack. Bytecode becomes useful when you are checking first-run costs, investigating a small benchmark, learning how closures and property operations are represented, or verifying a claim about what the engine does before optimization.

Practical bytecode reading checklist
QuestionBytecode can help when
StartupYou want to know what runs before hot-tier compilation has time to happen.
MemoryYou are studying why engines flush old bytecode or delay feedback vectors.
Calls and propertiesYou need exact operation shapes: property load, property call, free call, or closure context load.
TeachingYou want a concrete bridge from source code to interpreter state before studying JIT tiers.
What does this bytecode do?
  • `Ldar a0`
  • `Star0`
  • `Add r0, [1]`
  • `TestLessThan r1, [0]`
  • `JumpLoop [19], [0], [3]`
  • `Return`
  • `CallProperty1 r0, a0, r2, [7]`
  • `CreateClosure [1], [0], #2`
Try it yourself
0 of 8 correct

Sort each V8-looking operation by the state it mainly touches. Then read the explanations to reinforce the mental model.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Bytecode is JavaScript source with shorter names.” It is an engine-specific instruction format after parsing, scope work, and lowering.
  • “Bytecode is machine code.” The CPU does not execute Ignition bytecode directly; handlers interpret it or compilers translate from it.
  • “One V8 listing teaches every browser.” SpiderMonkey and JavaScriptCore have different bytecode formats and names.
  • “Feedback slots are constants.” Bracketed operands on many V8 bytecodes name feedback vector slots, not literal values.
  • “Flushing bytecode changes program behavior.” It is an internal memory optimization; if the function runs again, bytecode is regenerated.
Correcting similar-looking ideas
MistakeCorrection
Bytecode vs ASTASTs represent syntax; bytecode represents a lowered execution plan.
Bytecode vs machine codeBytecode is interpreted or compiled by the engine; machine code is CPU-specific.
Ignition vs all enginesIgnition is V8's interpreter; Firefox and Safari use different interpreters and bytecodes.
Toy VM vs V8The model teaches accumulator/register execution; it is not a V8 implementation.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upTrace one accumulator add

Predict the printed accumulator value.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const registers = { r0: 3 };
let acc = 4;
acc = registers.r0 + acc;
console.log(acc);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFollow a false jump

    What program-counter value prints after the conditional jump?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    let pc = 5;
    let acc = false;
    pc = acc ? pc + 1 : 12;
    console.log(pc);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName the free-call opcode

      Which exact call opcode appears for return fn(1, 2)?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeUse the current property opcode name

        Which opcode loads user.count in the stored V8 listing?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeExtend the toy dispatch table

          If the dispatch table had LdaSmi, Add, and jumps only, which handler name would you add for multiplication?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Why did V8 introduce Ignition bytecode instead of immediately compiling every function to baseline machine code?

              Choose an answer to see the explanation.

            2. Question 2 of 7In an Ignition listing, what does Add r0, [1] mean?

              Choose an answer to see the explanation.

            3. Question 3 of 7What does this tiny accumulator model print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const registers = { r0: 3 };
              let acc = 4;
              acc = registers.r0 + acc;
              console.log(acc);

              Choose an answer to see the explanation.

            4. Question 4 of 7What line in the stored loop listing proves that jumps use bytecode offsets?

              Read the code, then predictJavaScript
              [generated bytecode for function: loopSum (<addr> <SharedFunctionInfo loopSum>)]
              Bytecode length: 31
              Parameter count 2
              Register count 2
              Frame size 16
                207 S> <addr> @    0 : 0c                LdaZero
                       <addr> @    1 : c9                Star0
                220 S> <addr> @    2 : 0d 01             LdaSmi [1]
                       <addr> @    4 : c8                Star1
                234 S> <addr> @    5 : 0b 03             Ldar a0
                234 E> <addr> @    7 : 71 f8 00          TestLessThan r1, [0]
                       <addr> @   10 : 9e 12             JumpIfFalse [18] (<addr> @ 28)
                245 S> <addr> @   12 : 0b f8             Ldar r1
                259 E> <addr> @   14 : 3b f9 01          Add r0, [1]
                       <addr> @   17 : c9                Star0
                268 S> <addr> @   18 : 0b f8             Ldar r1
                274 E> <addr> @   20 : 47 01 02          AddSmi [1], [2]
                       <addr> @   23 : c8                Star1
                225 E> <addr> @   24 : 8e 13 00 03       JumpLoop [19], [0], [3] (<addr> @ 5)
                285 S> <addr> @   28 : 0b f9             Ldar r0
                298 S> <addr> @   30 : ae                Return
              Constant pool (size = 0)
              Handler Table (size = 0)
              Source Position Table (size = 27)
              <addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>

              Choose an answer to see the explanation.

            5. Question 5 of 7Which opcode sequence is accurate for a method call in Node 22.23.1 / V8 12.4?

              Read the code, then predictJavaScript
              [generated bytecode for function: propertyCall (<addr> <SharedFunctionInfo propertyCall>)]
              Bytecode length: 27
              Parameter count 2
              Register count 3
              Frame size 24
                124 S> <addr> @    0 : 2f 03 00 01       GetNamedProperty a0, [0], [1]
                130 E> <addr> @    4 : 47 01 00          AddSmi [1], [0]
                117 E> <addr> @    7 : 35 03 00 03       SetNamedProperty a0, [0], [3]
                149 S> <addr> @   11 : 2f 03 01 05       GetNamedProperty a0, [1], [5]
                       <addr> @   15 : c9                Star0
                160 E> <addr> @   16 : 2f 03 00 01       GetNamedProperty a0, [0], [1]
                       <addr> @   20 : c7                Star2
                149 E> <addr> @   21 : 61 f9 03 f7 07    CallProperty1 r0, a0, r2, [7]
                167 S> <addr> @   26 : ae                Return
              Constant pool (size = 2)
              <addr>: [TrustedFixedArray]
               - map: <addr> <Map(TRUSTED_FIXED_ARRAY_TYPE)>
               - length: 2
                         0: <addr> <String[5]: #count>
                         1: <addr> <String[5]: #label>
              Handler Table (size = 0)
              Source Position Table (size = 18)
              <addr> <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>

              Choose an answer to see the explanation.

            6. Question 6 of 7What happens when V8 flushes old bytecode for a function and that function runs again?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which comparison between engines is accurate?

              Choose an answer to see the explanation.

            Key takeaways

            • Bytecode is a compact execution format between source ASTs and CPU machine code.
            • Ignition uses virtual registers plus an implicit accumulator; many operands are omitted because the accumulator is assumed.
            • --print-bytecode listings show bytecode offsets, registers, constants, feedback slots, and metadata for one V8 version.
            • Handlers and dispatch are the interpreter's core: read an opcode, jump to its handler, update state, repeat.
            • Engines can flush cold bytecode and allocate feedback lazily to reduce memory without changing JavaScript behavior.
            • SpiderMonkey and JavaScriptCore share the bytecode-first strategy but use different formats and first-tier interpreters.

            Remember the one-liner.
            Bytecode lets an engine run quickly now, learn cheaply, and postpone expensive machine-code decisions until the code proves it deserves them.

            Next: Startup: snapshots & code caching. Then return to Tiered compilation to see how hot bytecode graduates beyond the interpreter.

            CompleteFrontend Clear concepts. Working examples.