JavaScript looks interpreted — you hand V8 text, things happen. But V8 doesn't run your text. It compiles it, in stages, and the first stage produces a compact instruction format called bytecode. Today's single win: read the bytecode V8 generates for a function you wrote, and understand why it exists.
Every JavaScript function travels down this assembly line inside V8. Today we stop at the second station — Ignition, the bytecode interpreter.
Ignition is a register machine that interprets bytecode. Most code only ever runs as bytecode — it's small and fast to produce. Only "hot" functions (run many times) get promoted to the optimizing compilers on the right, which emit real machine code. We'll meet those in later lessons. Source: Firing up the Ignition interpreter, Launching Ignition and TurboFan.
Ldar = "Load
Accumulator from Register", and Add adds to whatever's already
in the accumulator.
1Create a tiny script. In a terminal:
cat > /tmp/v8demo.js <<'EOF'
function add(a, b) { return a + b; }
add(1, 2);
EOF
2Ask V8 to show its work. The --print-bytecode
flag is a real V8 flag; Node passes it straight through to the embedded engine.
We filter to just our function:
node --print-bytecode --print-bytecode-filter=add /tmp/v8demo.js
You'll see something like this (addresses will differ — they're live memory pointers):
[generated bytecode for function: add (<SharedFunctionInfo add>)]
Bytecode length: 6
Parameter count 3
Register count 0
Frame size 0
21 S> @ 0 : 0b 04 Ldar a1
30 E> @ 2 : 3f 03 00 Add a0, [0]
34 S> @ 5 : b3 Return
Three instructions. That's the entirety of a + b. Here's what each does:
| Bytecode | What it means |
|---|---|
| Ldar a1 | Load Accumulator from Register a1.
a1 is the second argument, b. The accumulator now holds b. |
| Add a0, [0] | Add register a0 (the first argument, a) to the
accumulator. The accumulator now holds a + b. The [0] is a
feedback slot — index 0 in this function's feedback vector, where V8 records the
types it saw (two small integers). That recorded feedback is exactly what the
optimizing compilers read later to generate fast machine code. |
| Return | Return whatever is in the accumulator. |
add takes 2 args? The hidden first
parameter is the receiver — the value of this. Every JS function call
carries it, so V8 counts it as a parameter. The 21 S> / 30 E>
columns are source-position markers (Statement / Expression) that map bytecode
back to your source for debugging and stack traces.
Walkthrough: Understanding V8's Bytecode (F. Hinkelmann).
The whole point of recording type feedback is to promote hot functions to machine code.
You can force and observe it with --allow-natives-syntax, which unlocks
%-prefixed intrinsics (V8's internal test hooks — not real JS):
cat > /tmp/v8opt.js <<'EOF'
function add(a, b) { return a + b; }
add(1, 2);
%OptimizeFunctionOnNextCall(add); // tell V8: compile this now
add(3, 4); // ...runs as machine code
console.log(%GetOptimizationStatus(add).toString(2));
EOF
node --allow-natives-syntax /tmp/v8opt.js
The number printed is a bitfield. A bit meaning "optimized" being set tells you the function is no longer running as bytecode — TurboFan/Maglev took over. We'll decode this properly when we reach the optimizing tiers.
add contain a feedback slot [0]?Ldar a1
Add a0, [0]
Return
Register count 0? Want to see the bytecode for a for loop or an
object property access? Just ask in the chat. Try: "Show me the bytecode for a loop and
explain the jumps," or "What's a SharedFunctionInfo?"
You can now make V8 reveal its bytecode and read a simple function's instructions — the ground floor of its execution model. Next we'll map the whole assembly line and see when a function jumps from bytecode to machine code. Then we build V8 from source so you can drive it directly from C++.
Reference docs for this lesson: V8 Inspection Flags cheat sheet · V8 Glossary