Bytecode & interpreters
Learn why engines run bytecode first, how V8 Ignition reads it with an accumulator interpreter, and how other engines start executing.
- 01Explain the bytecode trade-offContrast AST walking, compact bytecode, and immediate machine code for startup, memory, and tiering.
- 02Read a V8 listingUse
--print-bytecodeoutput to identify offsets, operands, registers, feedback slots, constants, and jumps. - 03Trace 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.
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.
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, andr1
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 + MEMORYEngines 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.
| Form | What runs | Trade-off |
|---|---|---|
| AST interpreter | Walks tree nodes directly | Simple to build, but pointer-heavy trees cost memory and repeated tree walking is cache-unfriendly. |
| Register bytecode | Compact opcodes plus virtual registers | V8 Ignition and JSC-style bytecode keep locals in frame slots and use concise operands. |
| Stack bytecode | Opcodes push and pop an operand stack | SpiderMonkey JSOp bytecode records stack uses/defs and keeps instructions compact. |
| Machine code | CPU instructions for one architecture | Fast when worth compiling, but larger and slower to produce during startup. |
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, accIgnition 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.
| Name | How to read it |
|---|---|
a0, a1 | Function parameters. V8's parameter count includes the receiver, so one user parameter prints as Parameter count 2. |
r0…rN | Virtual registers in the interpreter frame for locals and temporaries. |
| Accumulator | Implicit input/output for many bytecodes, so operands stay short. |
| Constant pool | Strings, scope info, shared function info, and other constants referenced by index operands. |
[n] operands | Feedback 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."
| Group | Examples |
|---|---|
| Load/store | LdaZero, LdaSmi, Ldar, Star0, Star1, LdaImmutableCurrentContextSlot |
| Arithmetic and tests | Add, AddSmi, MulSmi, TestLessThan with feedback slots when V8 records runtime types. |
| Properties and calls | GetNamedProperty, SetNamedProperty, CallProperty1, CallUndefinedReceiver2. Older posts may show names such as LdaNamedProperty; Node 22/V8 12.4 prints the names here. |
| Control flow | JumpIfFalse, JumpLoop, Return use bytecode offsets, not source line numbers. |
| Closures | CreateFunctionContext, StaCurrentContextSlot, CreateClosure, and context-slot loads connect to scope analysis. |
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 OUTPUTYou 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>.
node --print-bytecode --print-bytecode-filter=loopSum bytecode-demo.jsfunction 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.
[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.
[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.
[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)>[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.
[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 V8The 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.
This is a model inspired by Ignition, not Ignition itself. Step through a loop and watch pc, the accumulator, and virtual registers change.
script
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));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.
A shorter run shows how expression temporaries move through the accumulator before a return.
script
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));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.
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 Return10loopSum(n)[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)>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.
Handlers and dispatch
OPCODE TO HANDLERA 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.
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 LITEBytecode 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.
| Mechanism | What to remember |
|---|---|
| Bytecode flushing | V8 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 demand | If a flushed function runs again, V8 recompiles its bytecode from source or cached data before executing. |
| Lazy feedback allocation | V8 delays FeedbackVector allocation until enough bytecode has executed; Node 22 exposes --lazy-feedback-allocation and an invocation threshold flag. |
| Source positions | V8 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 VMV8'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.
| Engine | First tier | Bytecode shape |
|---|---|---|
| V8 | Ignition | Register bytecode with an implicit accumulator; Sparkplug and optimizing tiers consume the bytecode/feedback path. |
| SpiderMonkey | Baseline Interpreter | JSOp bytecode is stack-based: opcode metadata declares stack uses and definitions; Baseline Interpreter interprets one opcode at a time and attaches ICs. |
| JavaScriptCore | LLInt | JSC 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 WORKYou 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.
| Question | Bytecode can help when |
|---|---|
| Startup | You want to know what runs before hot-tier compilation has time to happen. |
| Memory | You are studying why engines flush old bytecode or delay feedback vectors. |
| Calls and properties | You need exact operation shapes: property load, property call, free call, or closure context load. |
| Teaching | You want a concrete bridge from source code to interpreter state before studying JIT tiers. |
`Ldar a0``Star0``Add r0, [1]``TestLessThan r1, [0]``JumpLoop [19], [0], [3]``Return``CallProperty1 r0, a0, r2, [7]``CreateClosure [1], [0], #2`
Sort each V8-looking operation by the state it mainly touches. Then read the explanations to reinforce the mental model.
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.
| Mistake | Correction |
|---|---|
| Bytecode vs AST | ASTs represent syntax; bytecode represents a lowered execution plan. |
| Bytecode vs machine code | Bytecode is interpreted or compiled by the engine; machine code is CPU-specific. |
| Ignition vs all engines | Ignition is V8's interpreter; Firefox and Safari use different interpreters and bytecodes. |
| Toy VM vs V8 | The model teaches accumulator/register execution; it is not a V8 implementation. |
Practice exercises
5 EXERCISESPredict the printed accumulator value.
const registers = { r0: 3 };
let acc = 4;
acc = registers.r0 + acc;
console.log(acc);The register holds 3 and the accumulator holds 4, so the model prints 7.
What program-counter value prints after the conditional jump?
let pc = 5;
let acc = false;
pc = acc ? pc + 1 : 12;
console.log(pc);Because acc is false, the conditional expression chooses 12, so the program counter prints 12.
Which exact call opcode appears for return fn(1, 2)?
Node 22.23.1 / V8 12.4 prints CallUndefinedReceiver2 for this free function call with two arguments.
Which opcode loads user.count in the stored V8 listing?
The current V8 12.4 listing uses GetNamedProperty for the named property load.
If the dispatch table had LdaSmi, Add, and jumps only, which handler name would you add for multiplication?
const handlers = {
Mul(state, register) {
state.acc = state.registers[register] * state.acc;
state.pc += 1;
},
};A Mul handler reads a register and the accumulator, writes the product back to the accumulator, and advances pc.
Check your understanding
7 QUESTIONSQuestion 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.
Question 2 of 7In an Ignition listing, what does
Add r0, [1]mean?Choose an answer to see the explanation.
Question 3 of 7What does this tiny accumulator model print?
Read the code, then predictconst registers = { r0: 3 }; let acc = 4; acc = registers.r0 + acc; console.log(acc);Choose an answer to see the explanation.
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.
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.
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.
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-bytecodelistings 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.