cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Baseline compilers

Learn how baseline compilers such as V8 Sparkplug turn bytecode into quick machine code, reuse interpreter frames, and trade speed for quality.

By the end, you can
  • 01
    Explain Sparkplug's fast pathDescribe how V8 compiles Ignition bytecode directly to simple machine code without building an optimizer IR first.
  • 02
    Compare baseline tiers across enginesPlace V8 Sparkplug beside SpiderMonkey's Baseline Interpreter and Baseline Compiler, and JavaScriptCore's Baseline JIT.
  • 03
    Reason about the trade-offPredict when removing interpreter overhead is worth a quick compile, and when a later optimizing compiler is the better topic to study next.

A quick compiler between tiers

After inline caches, the engine has useful feedback at many operation sites. The next question is economic: how much compiler work should the engine spend before a function has proved it is very hot?

A baseline compiler answers “a little.” It turns already-created bytecode into simple machine code quickly. The code is not the engine's best possible code, but it removes interpreter overhead: repeated bytecode decoding, dispatch to the next bytecode handler, and some stack shuffling.

Definition

A baseline compiler is a fast, non-optimizing JIT tier that compiles bytecode into simple native code so a function can run with less interpreter overhead while heavier optimizers wait for hotter evidence.

This lesson stays in the baseline layer. Tiered compilation introduced the whole ladder. Optimizing compilers covers Maglev, TurboFan, Warp, DFG, and FTL in depth. A later speculation and deoptimization lesson covers what happens when optimized assumptions fail.

Sparkplug walks bytecode directly

V8

In V8, Ignition is the bytecode interpreter. Sparkplug is V8's baseline compiler. The public V8 article describes Sparkplug as compiling Ignition bytecode directly to machine code, without building a large intermediate representation, or IR. An IR is a compiler's private form of the program used for analysis and rewrites.

Small function Sparkplug can compile from bytecodePop out in the code editor (opens in a new tab)JavaScript
function stamp(total) {  const fee = 2;  return total + fee;} console.log(stamp(40));

Line 1 defines stamp. Line 2 creates fee. Line 3 adds the argument total to that local value. Line 6 calls the function and prints 42. Ignition first lowers the function to bytecode; Sparkplug can then walk that bytecode instead of reparsing the JavaScript source.

Stable bytecode shape asserted by the lesson testsText
[generated bytecode for function: stamp]
LdaSmi [2]
Star0
Ldar r0
Add a0, [0]
Return

Read the bytecode in plain language: LdaSmi [2] loads the small integer 2, Star0 stores it in virtual register 0, Ldar r0 loads that register, Add a0, [0] adds the first argument with feedback slot 0, and Return returns the accumulator.

Sparkplug's direct-bytecode design
PartWhat it means
InputIgnition bytecode, already parsed and lowered from source.
Compiler shapeA mostly linear bytecode walk with no heavy intermediate representation.
Generated codeSimple native code that often calls existing builtins for JavaScript semantics.
Main winInterpreter dispatch and bytecode decoding overhead are precompiled away.
Real-life analogyA cook who knows the tea recipe

A cook who has made the same tea many times stops reading every recipe step. They work through familiar steps quickly and ask for help with an unusual order. That is the baseline mindset.

In real life: The tea recipe is already written
In JavaScript: Ignition bytecode already exists
In real life: The cook knows each usual step
In JavaScript: Sparkplug emits a fixed template for each bytecode
In real life: An unusual order gets extra help
In JavaScript: Complex JavaScript semantics call shared builtins
In real life: Tea is ready fast, without a new recipe
In JavaScript: Baseline code is quick, not deeply optimized

Where the analogy stops: A cook chooses steps. Sparkplug emits architecture-specific machine code, and its exact templates are V8 implementation details.

Template compilation model

STEP THROUGH

The next walkthrough is a teaching model. It does not inspect your browser's engine. It shows the same shape of work Sparkplug is famous for: one bytecode instruction comes in, one simple machine-code-like template goes out.

The template compiler modelPop out in the code editor (opens in a new tab)JavaScript
const bytecode = [  ["LdaSmi", 2],  ["Star", "fee"],  ["Ldar", "fee"],  ["Add", "total"],  ["Return"],]; function compileTemplates(bytecodes) {  const machine = [];  for (const instruction of bytecodes) {    const [op, operand] = instruction;    if (op === "LdaSmi") machine.push(`load_smi ${operand}`);    if (op === "Star") machine.push(`store ${operand}`);    if (op === "Ldar") machine.push(`load ${operand}`);    if (op === "Add") machine.push(`add ${operand}`);    if (op === "Return") machine.push("return accumulator");  }  return machine;} console.log(compileTemplates(bytecode).join(" | "));

Line 1 holds toy bytecode. Line 9 starts a compiler function. Line 11 walks the list in order. Lines 13 through 17 append fixed templates. Line 19 returns the templates, and line 22 prints them as one trace.

Template walkthrough: bytecode becomes quick native templates
Step 0 of 8Ready
Your turn: follow the blue line

This teaching model turns a short bytecode list into machine-code-like templates. It does not inspect your browser's engine.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  ["LdaSmi", 2],  ["Star", "fee"],  ["Ldar", "fee"],  ["Add", "total"],  ["Return"],]; function compileTemplates(bytecodes) {  const machine = [];  for (const instruction of bytecodes) {    const [op, operand] = instruction;    if (op === "LdaSmi") machine.push(`load_smi ${operand}`);    if (op === "Star") machine.push(`store ${operand}`);    if (op === "Ldar") machine.push(`load ${operand}`);    if (op === "Add") machine.push(`add ${operand}`);    if (op === "Return") machine.push("return accumulator");  }  return machine;} console.log(compileTemplates(bytecode).join(" | "));
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.

Notice what is missing: there is no whole-program graph, no inlining decision, and no speculative rewrite. That is why the compiler is fast. It is also why this tier leaves peak performance to later optimizers.

Interpreter-compatible frames

FRAMES

A stack frame is the runtime storage for one active function call: its arguments, local slots, metadata, and return path. Sparkplug deliberately keeps frames compatible with Ignition. That means the debugger, profiler, stack traces, and on-stack replacement can keep using interpreter-shaped knowledge.

Real-life analogyA notebook with the same headings

A notebook is easier to scan when every page has the same headings. A quicker way of writing can keep those headings, even when one heading holds a new kind of note. Sparkplug takes a similar path with frames.

In real life: Each page has the same headings
In JavaScript: The interpreter frame has slots for function, context, metadata, and registers
In real life: A quicker note uses those headings too
In JavaScript: Baseline code stores values in compatible slots
In real life: One heading holds a different note
In JavaScript: Sparkplug reuses the offset slot for the feedback vector while mapping code addresses to offsets
In real life: Anyone can still read the page
In JavaScript: Stack walkers, debuggers, and OSR logic still understand the frame

Where the analogy stops: Notebook pages are paper. Engine stack frames are memory layouts, and each engine chooses its own exact slots.

Interpreter frame and Sparkplug frame modelPop out in the code editor (opens in a new tab)JavaScript
const frame = {  functionName: "stamp",  context: "checkout",  argument0: 40,  bytecodeArray: "stamp bytecode",  offsetOrFeedback: "bytecode offset 0",  register0: undefined,}; function runInIgnition(total) {  frame.argument0 = total;  frame.offsetOrFeedback = "bytecode offset 0";  frame.register0 = 2;  return frame.argument0 + frame.register0;} function runInSparkplug(total) {  frame.argument0 = total;  frame.offsetOrFeedback = "feedback vector";  frame.register0 = 2;  return frame.argument0 + frame.register0;} console.log(runInIgnition(40));console.log(runInSparkplug(40));console.log(frame.register0);
Frame walkthrough: Sparkplug keeps interpreter-friendly slots
Step 0 of 11Ready
Your turn: follow the blue line

Compare an interpreter-shaped frame and a Sparkplug-shaped frame. This is a teaching model based on V8's public Sparkplug article, not live stack inspection.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  functionName: "stamp",  context: "checkout",  argument0: 40,  bytecodeArray: "stamp bytecode",  offsetOrFeedback: "bytecode offset 0",  register0: undefined,}; function runInIgnition(total) {  frame.argument0 = total;  frame.offsetOrFeedback = "bytecode offset 0";  frame.register0 = 2;  return frame.argument0 + frame.register0;} function runInSparkplug(total) {  frame.argument0 = total;  frame.offsetOrFeedback = "feedback vector";  frame.register0 = 2;  return frame.argument0 + frame.register0;} console.log(runInIgnition(40));console.log(runInSparkplug(40));console.log(frame.register0);
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.
Why compatible frames make a baseline compiler easier to integrate
Frame ideaWhy it matters
Interpreter frameStores function, context, argument count, bytecode array, bytecode offset, and virtual registers.
Sparkplug frameKeeps the same register-style layout, reusing the offset slot for the feedback vector while code-address maps recover offsets.
Why it mattersDebugging, stack walking, OSR, and tier switching can reuse interpreter machinery with little translation.

SpiderMonkey and JavaScriptCore

ENGINES

Baseline compilation is not only a V8 idea. SpiderMonkey, Firefox's engine, documents a C++ interpreter, a Baseline Interpreter, a Baseline Compiler, and then Ion through Warp. Its Baseline Interpreter is a JIT-compiled interpreter that can use inline caches. Its Baseline Compiler is a simple template JIT that emits per-script native code and uses the same ICs.

JavaScriptCore, Safari's engine, publicly describes a tier sequence of LLInt, Baseline JIT, DFG, and FTL. LLInt is the low-level interpreter. Baseline JIT is the quick native tier. DFG and FTL are the optimizing tiers, linked forward to the next lesson.

Baseline tiers across major engines
EngineInterpreter sideBaseline sideWhat to remember
V8IgnitionSparkplugCompiles bytecode directly and keeps interpreter-compatible frames.
SpiderMonkeyC++ interpreter, then Baseline InterpreterBaseline CompilerThe Baseline Interpreter and Baseline Compiler share ICs; the compiler generates per-script native code.
JavaScriptCoreLLIntBaseline JITJSC's public overview names LLInt, Baseline JIT, DFG, and FTL as its tier sequence.
Primary source trail

The claims here come from the V8 Sparkplug article, Mozilla's “How SpiderMonkey Optimizes” and CacheIR docs, and WebKit's JavaScriptCore speculation article. The exact thresholds and internals can change, so the article teaches roles, not portable promises.

Which tier does this work belong to?
  • Decode the next bytecode and dispatch to its handler every time.
  • Walk bytecode and emit simple native templates without building a big IR graph.
  • Emit a call to a shared builtin for complex JavaScript behavior.
  • Use feedback to inline a stable hot function call.
  • Keep a bytecode offset so the current interpreter position is known.
  • Build an SSA graph and run global optimization passes.
Try it yourself
0 of 6 correct

Sort each card by whether it describes interpreter work, baseline compilation, or optimizing compilation.

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

Compile speed vs code quality

TRADE-OFF

A baseline compiler chooses compile speed over code quality. That does not mean it creates bad code. It means it refuses expensive analysis until the function has earned it. The code is better than interpretation for repeated execution, but it usually does not inline many functions, rebuild the program as SSA, or specialize every representation.

Baseline compiler versus optimizing compiler
QuestionBaseline compilerOptimizing compiler
GoalRemove interpreter overhead quicklySearch for better code using feedback and assumptions
Compile timeVery low; suitable soon after warm-upHigher; must pay back on very hot code
Code qualityGood enough: simple control flow and builtin callsHigher peak speed through inlining, representation choices, and broader analysis
Best lesson linkThis lessonOptimizing compilers and speculation/deopt

This is why baseline compilers are perfect for the middle: code that is too active for interpretation to be ideal, but not proven hot enough for a top-tier optimizer. If a function becomes very hot, the next lesson explains the optimizing compilers that spend more time for better machine code.

Try the cost trade-off

PLAYGROUND

Use this model to change one input: how many times a function runs. The numbers are not V8 thresholds. They are a small model for the curve: interpreter has no compile cost, baseline has a small compile cost and lower call cost, and optimizing has a large compile cost with the lowest call cost.

How many calls make a baseline compiler worth it?
Cost model used by the playgroundPop out in the code editor (opens in a new tab)JavaScript
function total(compileCost, callCost, calls) {  return compileCost + callCost * calls;} for (const calls of [3, 20, 200]) {  console.log(    calls,    total(0, 5, calls),    total(12, 2, calls),    total(80, 0.7, calls).toFixed(1),  );}
20 callsBaseline compiler is lowest
Interpreter100.0

No compile bill, but every call pays dispatch overhead.

compile
0.0
run
100.0
Baseline compiler52.0

Small compile bill removes dispatch without deep optimization.

compile
12.0
run
40.0
Optimizing compiler94.0

Large compile bill can win only when the function is very hot.

compile
80.0
run
14.0
Try it yourself

Baseline compiler wins for 20 calls in this teaching model: 12.0 compile units plus 40.0 run units equals 52.0 total. Real engines use feedback, memory budgets, and background threads; this slider only teaches the shape of the trade-off.

This is a teaching model; it does not inspect your browser's engine or predict exact V8 thresholds. Change the call count and compare total simulated cost.

For three calls, the interpreter's zero compile cost can win. Around twenty calls in this model, baseline wins. At two hundred calls, the optimizing tier finally repays its big compile bill. Real engines use richer counters, inline caches, type feedback, memory budgets, and background compilation.

Node and V8 evidence

VERIFIED

The lesson tests probe Node 22 on V8 12.4 without depending on exact patch numbers, addresses, or timings. They check that Sparkplug flags exist, that --print-bytecode shows the bytecode above, that %CompileBaseline exists, and that the V8 status mask includes kBaseline after forced baseline compilation.

Node-only baseline probeJavaScript
// Run in Node with --allow-natives-syntax.const bits = { kIsFunction: 1 << 0, kBaseline: 1 << 15 };function stamp(total) {  const fee = 2;  return total + fee;} %CompileBaseline(stamp);console.log(stamp(40));const status = %GetOptimizationStatus(stamp);console.log(Boolean(status & bits.kIsFunction));console.log(Boolean(status & bits.kBaseline));
Shell commands used as stable evidenceShell
node --v8-options | grep -E -- "--sparkplug|--always-sparkplug|--trace-baseline-batch-compilation|--print-bytecode"
node --print-bytecode --print-bytecode-filter=stamp stamp-probe.mjs
node --allow-natives-syntax --always-sparkplug baseline-status.mjs
node --allow-natives-syntax --no-sparkplug baseline-status.mjs
Verified runtime fact

In Node 22 with V8 12.4, --sparkplug is enabled by default, --always-sparkplug, --no-sparkplug, --print-bytecode, and --trace-baseline-batch-compilation are available, and %CompileBaseline(stamp) sets the kBaseline status bit. This is V8-specific diagnostic evidence, not browser JavaScript.

Use baseline knowledge safely

PRACTICAL

A working developer rarely needs to know whether a function is in Sparkplug, Baseline JIT, or another baseline tier. The useful habit is to understand why early runs can differ from warm runs, why medium-hot helpers can speed up without becoming top-tier optimized, and why stable shapes from the previous lesson still help later tiers.

Practical decisions influenced by baseline compilers
SituationBaseline lessonWhat to do
Short page helpersThey may run in an interpreter or baseline tier only.Optimize startup clarity before chasing peak JIT speed.
Medium-hot helpersBaseline code can remove dispatch overhead without waiting for a heavy optimizer.Keep shapes and value types stable enough for the engine to see patterns.
MicrobenchmarksFirst samples can include bytecode, baseline compile, and later tier-up.Warm up, measure several samples, and report what you actually timed.
Production codeTier names and thresholds are implementation details.Do not call V8 native syntax or write code for one compiler.

Measure first. If a user-visible path is slow, profile the path in the browser or Node process that runs it. Keep the code readable, keep hot data reasonably consistent, and treat V8 native syntax as a lab tool only.

Common misconceptions

“Baseline means low quality and useless.”

No. Baseline code can be a big win because it removes interpreter overhead quickly.

“Sparkplug optimizes like TurboFan.”

No. Sparkplug is deliberately simple. TurboFan and newer optimizing pipelines do deeper analysis.

“Interpreter-compatible frames are JavaScript semantics.”

No. They are an engine strategy. JavaScript semantics are defined by the language specification, not by V8's frame layout.

“All engines use Sparkplug.”

No. Sparkplug is V8's name. SpiderMonkey and JavaScriptCore use their own tier names and implementations.

“I should force baseline compilation in production.”

No. Native syntax needs special flags and is not portable JavaScript. Measure user behavior instead.

Ideas that are easy to confuse
IdeaMeansDoes not mean
Baseline compilerA quick native-code tier that removes interpreter overhead.A top-tier optimizer that performs deep global rewrites.
BytecodeThe compact instruction stream a baseline compiler can walk.Machine code already ready for the CPU.
Interpreter-compatible frameA stack layout that lets tiers share debugger and OSR machinery.A guarantee that every engine uses V8's exact frame slots.
V8 native syntaxA local testing interface behind flags.Portable JavaScript for application code.

Practice exercises

6 EXERCISES
Exercise 1 · Warm-upRead the template output

What is the first machine-code-like template emitted by the toy compiler?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const bytecode = [["LdaSmi", 2], ["Return"]];
console.log("load_smi 2");

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

    Exercise 2 · PracticeFind the bytecode add

    Which bytecode line adds the argument to the local fee value?

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

      Exercise 3 · PracticePredict the baseline cost

      What number does the baseline cost model print for 20 calls?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function total(compileCost, callCost, calls) {
        return compileCost + callCost * calls;
      }
      console.log(total(12, 2, 20));

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

        Exercise 4 · PracticeDecode the status bit

        Which V8 status bit name proves forced baseline compilation in the Node probe?

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

          Exercise 5 · PracticeSpot the frame slot change

          Which value replaces the bytecode offset slot in the Sparkplug frame model?

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

            Exercise 6 · ChallengeApply this to a real app

            A checkout helper feels slow on first interaction. What should you do before rewriting it around a JIT theory?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            // A checkout helper feels slow on first interaction.
            // What should you do before rewriting it for a JIT tier?

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

              Check your understanding

              Answer these without guessing from tier names. For code questions, compute the JavaScript output first, then connect it back to the engine idea.

              Lesson quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is the main job of a baseline compiler?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this Sparkplug teaching snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function stamp(total) {
                  const fee = 2;
                  return total + fee;
                }
                console.log(stamp(40));

                Choose an answer to see the explanation.

              3. Question 3 of 8Which Sparkplug design choice keeps compilation fast?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does this cost model print for baseline at 20 calls?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function total(compileCost, callCost, calls) {
                  return compileCost + callCost * calls;
                }
                console.log(total(12, 2, 20));

                Choose an answer to see the explanation.

              5. Question 5 of 8Why are interpreter-compatible frames useful?

                Choose an answer to see the explanation.

              6. Question 6 of 8Which statement matches SpiderMonkey's baseline tiers?

                Choose an answer to see the explanation.

              7. Question 7 of 8Which JavaScriptCore tier comes after LLInt in the public JSC overview?

                Choose an answer to see the explanation.

              8. Question 8 of 8What practical rule follows from baseline compilers?

                Choose an answer to see the explanation.

              Key takeaways

              • Sparkplug is V8's fast baseline compiler: it compiles Ignition bytecode directly and avoids a heavy IR.
              • Interpreter-compatible frames let baseline code switch tiers, support OSR, and keep debugging machinery simple.
              • SpiderMonkey and JavaScriptCore have their own baseline tiers with similar roles but different names and internals.
              • Baseline compilation trades peak code quality for compile speed; measure real code before acting on any JIT theory.
              One-line summary

              Baseline compilers are the quick middle tier: they turn bytecode into simple native code fast enough to remove interpreter overhead before deeper optimizers are worth their cost.

              Next, Optimizing compilers shows what engines do when a function becomes hot enough for Maglev, TurboFan, Warp, DFG, or FTL to spend more time chasing better machine code.

              CompleteFrontend Clear concepts. Working examples.