completefrontend
Open lab My progress
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 how Sparkplug worksDescribe how V8's Sparkplug turns bytecode straight into simple machine code, one step at a time, without first rebuilding the program in its own form.
  • 02
    Compare baseline tiers in different enginesPut V8's Sparkplug next to SpiderMonkey's Baseline Interpreter and Baseline Compiler, and JavaScriptCore's Baseline JIT.
  • 03
    Weigh the costWork out when a quick compile pays back by making each call faster, and when a stronger, slower compiler is worth it instead.

A quick compiler in the middle

A JavaScript engine is the program inside a browser or Node that runs your code. It does not run a function the same way forever. It has several tiers: steps that go from a quick start to fast code. A function starts on the first step. If it runs very often, it can move up. A function that runs very often is called hot.

On the first step, the interpreter runs bytecode. Bytecode is a simple list of small steps that the engine made from your code. The interpreter works like this, again and again:

  1. It reads the next bytecode step.
  2. It finds the piece of code that handles that kind of step. This is called dispatch.
  3. It runs that piece of code.

The reading and finding take time on every step of every call. That extra time is the interpreter's overhead.

To compile means to turn code into another form before running it. Machine code is the form the computer's processor runs directly. A baseline compiler turns bytecode into simple machine code, and it does this quickly. The result is not the fastest code the engine can make. But now nothing has to read and look up each bytecode step on every call.

Three steps in a row: the interpreter, with no compile step and slow calls. The baseline compiler, with a quick compile and faster calls. The optimizer, with a slow compile and the fastest calls.interpreterno compile stepslow callsbaselinequick compilefaster callsoptimizerslow compilefastest calls
The baseline compiler sits in the middle. It spends a little time compiling, so each call runs faster than in the interpreter.
Definition

A baseline compiler is a quick compiler that turns bytecode into simple machine code. It does not try to make the fastest code. It is a JIT ("just in time") compiler: it compiles while the program is already running. It removes the interpreter's overhead, while the slower, stronger compilers wait until a function has run many more times.

In the last lesson, inline caches, the engine saved facts about each operation in your code. Now the question is about cost. How much time should the engine spend compiling a function before that function has shown that it is hot? A baseline compiler's answer is "only a little."

This lesson stays on the baseline step. Tiered compilation showed all the steps. Optimizing compilers covers the strongest compilers, such as V8's TurboFan. A later lesson covers what happens when an optimizing compiler's guess turns out wrong.

Sparkplug reads bytecode directly

V8

V8 is the engine in Chrome and Node. Its interpreter is called Ignition. Its baseline compiler is called Sparkplug. Sparkplug does not read your JavaScript source. It starts from the bytecode that Ignition already made.

Most compilers first rebuild the program in their own private form. That form is called an intermediate representation, or IR. The IR helps a compiler study the code and change it, but building it takes time. V8's public article on Sparkplug says Sparkplug skips this. It turns each bytecode step straight into machine code.

A small function for Sparkplug to compilePop 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 starts a function called stamp. Line 2 makes a local value, fee, set to 2. Line 3 adds the argument total to fee and returns the sum. Line 6 calls stamp(40) and prints 42.

Before the function runs, Ignition turns it into bytecode. Here is the bytecode Node prints for stamp. The lesson tests check these lines.

The bytecode V8 makes for stampText
[generated bytecode for function: stamp]
LdaSmi [2]
Star0
Ldar r0
Add a0, [0]
Return

Two words first. A register is a numbered spot where the interpreter keeps a value. The accumulator is one special spot that holds the current result. In plain words, line by line:

  1. Line 1 is a heading: the bytecode below belongs to stamp.
  2. Line 2, LdaSmi [2], puts the small whole number 2 in the accumulator. "Smi" is short for "small integer".
  3. Line 3, Star0, copies the accumulator into register 0. Register 0 is fee.
  4. Line 4, Ldar r0, copies register 0 back into the accumulator.
  5. Line 5, Add a0, [0], adds the first argument, a0, which is total. The [0] names feedback slot 0: the place where the engine keeps notes about the values this add has seen.
  6. Line 6, Return, returns the value in the accumulator.

Some steps do complicated work, like an add that might get strings instead of numbers. For those, Sparkplug's machine code calls a builtin: a ready-made piece of machine code inside the engine that does one common job. Ignition calls the same builtins. That keeps Sparkplug small and quick.

The table below sums up how Sparkplug is built. Read each row as one part and what Sparkplug does there.

How Sparkplug is built
Part of the designWhat Sparkplug does
What it starts fromIt starts from Ignition's bytecode, which is already made from your code.
How it is builtIt reads the bytecode steps in order, once, and builds no IR.
What code it makesIt makes simple machine code that often calls builtins for the complicated parts.
What it savesNothing has to read each bytecode step and look up how to run it on every call.
Real-life analogyA cook who knows the tea recipe

A cook has made the same tea many times. They no longer read each line of the recipe. They do the usual steps quickly, from memory. When someone orders something unusual, they ask the kitchen for help. They never stop to write a new, better recipe.

In real life: The tea recipe is already written
In JavaScript: Ignition has already made the bytecode
In real life: The cook knows each usual step by heart
In JavaScript: Sparkplug has one fixed piece of machine code for each kind of bytecode step
In real life: An unusual order gets help from the kitchen
In JavaScript: Complicated steps call a builtin
In real life: The tea is ready fast, with no new recipe
In JavaScript: Baseline code is ready fast, but it is not the fastest code

A small template compiler, step by step

STEP THROUGH

A template is a fixed piece of code with a blank to fill in. A template compiler keeps one template for each kind of bytecode step. For each step it reads, it copies the matching template and fills in the blank. Sparkplug works this way.

The code below is a small model in plain JavaScript. It does not look inside your browser's engine. The toy bytecode uses names like fee instead of register numbers, so it is easier to read. The output strings stand in for machine code.

A small template compilerPop 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(" | "));

In plain words, line by line:

  1. Lines 1 to 7 hold the toy bytecode: five steps, the same five as the real bytecode for stamp.
  2. Line 9 starts the compiler function, compileTemplates.
  3. Line 10 makes an empty list, machine, for the output.
  4. Line 11 goes through the steps in order, one at a time.
  5. Line 12 splits each step into its name, op, and its value, operand.
  6. Lines 13 to 17 check the name and add the matching template to the list.
  7. Line 19 returns the list. Line 22 prints it with | between the templates: load_smi 2 | store fee | load fee | add total | return accumulator.
Five bytecode steps on the left, each with an arrow to one template on the right. LdaSmi 2 becomes load_smi 2. Star fee becomes store fee. Ldar fee becomes load fee. Add total becomes add total. Return becomes return accumulator.bytecode steptemplateLdaSmi 2load_smi 2Star feestore feeLdar feeload feeAdd totaladd totalReturnreturn accumulator
The toy compiler reads the five steps in order. Each step turns into one fixed template. Nothing is planned across the whole function.
Step through: each bytecode step becomes one template
Step 0 of 8Ready
Your turn: follow the blue line

This small model turns a short list of bytecode steps into templates that stand in for machine code. It does not look inside your browser's engine.

Running in
  1. script
Next: line 1
  ["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(" | "));
CallStoreChangeResultBlue line: runs next. 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 the compiler does not do. It never looks at the whole function at once. It does not join steps together or make guesses about the values. That is why it is fast. It is also why its code is not the fastest possible. The stronger compilers do that extra work later.

Frames that match the interpreter

FRAMES

When a function is called, the engine sets up a stack frame for that call. A stack frame is a block of memory with fixed spots, called slots. Its slots hold:

  • the function being run, and its context: the outer variables it can reach,
  • the arguments,
  • a few extra facts about the call, such as where it is in the bytecode,
  • the registers,
  • the way back to the code that made the call.

Sparkplug keeps its frames in the same shape as Ignition's frames. That matters, because many other parts of the engine read frames:

  • the debugger, which shows you the variables when you pause,
  • the profiler, which measures where the time goes,
  • stack traces in error messages,
  • on-stack replacement, or OSR: moving a function to another tier while it is still running, for example in the middle of a long loop.

Because the frames match, all of these keep working with Sparkplug code. They need little or no new code.

Real-life analogyA notebook with the same headings

A notebook is easy to read when every page has the same headings. Someone who writes faster can keep those same headings. Under one heading they may write a different kind of note, but every reader still knows where to look.

In real life: Each page has the same headings
In JavaScript: The interpreter frame has slots for the function, context, extra facts, and registers
In real life: A faster writer uses the same headings
In JavaScript: Baseline code keeps its values in the same slots
In real life: One heading holds a different note
In JavaScript: Sparkplug keeps the feedback vector in the slot for the bytecode offset
In real life: Anyone can still read the page
In JavaScript: The debugger, stack traces, and OSR still understand the frame
One frame, filled by the interpreter and by SparkplugPop 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);

This is a small model of a frame. In plain words, line by line:

  1. Lines 1 to 8 make one frame object. Each property stands for one slot.
  2. Line 6, offsetOrFeedback, is the slot that the two tiers use in different ways.
  3. Lines 10 to 15, runInIgnition, fill the slots the way the interpreter does. Line 12 puts a bytecode offset in the special slot. A bytecode offset is the position of the current step in the bytecode.
  4. Lines 17 to 22, runInSparkplug, fill the same slots. Line 19 puts the feedback vector in that slot instead. The feedback vector is the list of notes the engine keeps for this function.
  5. Lines 24 to 26 print 42, 42, and 2.

Sparkplug can still find the bytecode position when it needs it. It keeps a map from each place in its machine code back to the matching bytecode offset.

Two frames side by side. The interpreter frame holds argument0: 40, bytecode offset 0, and register0: 2. The Sparkplug frame holds argument0: 40, feedback vector, and register0: 2. Only the middle slot changed.interpreter frameSparkplug frameargument0: 40sameargument0: 40bytecode offset 0changedfeedback vectorregister0: 2sameregister0: 2
Both frames have the same slots in the same order. Only the middle slot holds something different: Sparkplug keeps the feedback vector there.
Step through: Sparkplug fills the same frame slots
Step 0 of 11Ready
Your turn: follow the blue line

Compare a frame filled by the interpreter with one filled by Sparkplug. This is a small model based on V8's public Sparkplug article. It does not look at a real stack.

Running in
  1. script
Next: line 1
  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);
CallStoreChangeResultBlue line: runs next. 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 table below shows why matching frames help. Read each row as one frame idea and what it means.

Why frames that match make a baseline compiler easier to add
Frame ideaWhat it means
Interpreter frameIt holds the function, the context, the number of arguments, the bytecode, the bytecode offset, and the registers.
Sparkplug frameIt keeps the same slots, but puts the feedback vector where the bytecode offset was. A map from its machine code finds the offset when needed.
Why it helpsThe debugger, stack traces, OSR, and moving between tiers can reuse the interpreter's code with few changes.

Baseline tiers in other engines

ENGINES

Other engines have baseline tiers too. SpiderMonkey is the engine in Firefox. Its documents list these tiers, in order:

  1. a plain interpreter, written in C++,
  2. the Baseline Interpreter,
  3. the Baseline Compiler,
  4. Warp and Ion, which together make the optimizing compiler.

The Baseline Interpreter is an interpreter that the engine itself made as machine code. Both baseline tiers use inline caches, or ICs: the small saved shortcuts for operations from the last lesson. The Baseline Compiler is a simple template compiler, like Sparkplug. It makes machine code for each script, and it uses the same ICs.

JavaScriptCore is the engine in Safari. Its public overview names four tiers in order: LLInt, Baseline JIT, DFG, and FTL. LLInt is the low-level interpreter. Baseline JIT is the quick compiler. DFG and FTL are the optimizing compilers; the next lesson covers them.

The table below puts the three engines side by side. Read each row as one engine, from its interpreter to its baseline tier.

Baseline tiers in three engines
EngineInterpreter tierBaseline tierWhat to remember
V8IgnitionSparkplugSparkplug turns bytecode straight into machine code and keeps frames that match the interpreter.
SpiderMonkeyA C++ interpreter, then the Baseline InterpreterBaseline CompilerBoth baseline tiers share the same inline caches. The compiler makes machine code for each script.
JavaScriptCoreLLIntBaseline JITIts public overview lists the tiers as LLInt, Baseline JIT, DFG, and FTL.
Where these facts come from

The facts here come from V8's Sparkplug article, Mozilla's "How SpiderMonkey Optimizes" and CacheIR documents, and WebKit's article on speculation in JavaScriptCore. Exact numbers and inner details can change from one version to the next. So this lesson teaches what each tier is for, not details that stay the same everywhere.

Which tier does this work belong to?
  • Read the next bytecode step and look up how to run it, every time.
  • Read the bytecode steps in order and copy a simple template for each, without building an IR.
  • Call a ready-made builtin for a complicated step.
  • Use the notes about values to copy a small, often-called function into its caller.
  • Keep the bytecode offset in the frame, so the current position is known.
  • Rebuild the whole function as an SSA graph and improve it in several passes.
Try it yourself
0 of 6 correct

Put each card in the tier that does that work: the interpreter, the baseline compiler, or the optimizing compiler.

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

Quick to compile, or fast to run

TRADE-OFF

A baseline compiler picks compile speed over code quality. Its code is not bad. It just skips slow, careful work until a function has shown that it is hot. Here are three kinds of work it usually skips:

  • Inlining: copying a called function's body into the caller, so the call itself goes away.
  • SSA form: rebuilding the whole function as a graph where each value is set only once, so it is easier to study. SSA stands for "static single assignment".
  • Special code for each kind of value, such as code that only handles small whole numbers.

Its code still runs faster than the interpreter when a function runs again and again.

The table below compares the two kinds of compiler. Read each row as one question, answered for each one.

Baseline compiler and optimizing compiler, side by side
QuestionBaseline compilerOptimizing compiler
What is it trying to do?Remove the interpreter's overhead, quickly.Make the fastest code it can, using notes and guesses about the values.
How long does it take to compile?Very little time, so it can start soon.More time, so it must win that time back on code that runs very often.
How fast is its code?Fast enough: simple steps and calls to builtins.Faster: it copies small functions into their callers, picks the best form for each value, and studies the whole function.
Where can I learn more?In this lesson.In the lessons on optimizing compilers and on what happens when their guesses are wrong.

So baseline compilers fit the middle. Some code runs too often for the interpreter to be a good choice, but not often enough to pay for an optimizing compiler. If a function keeps running even more, the next lesson shows what the optimizing compilers do.

Try the cost trade-off

PLAYGROUND

The playground below changes one input: how many times a function runs. The costs are made-up units, not real V8 numbers. Each tier has two costs:

  • a compile cost, paid once: 0 for the interpreter, 12 for baseline, and 80 for the optimizer,
  • a cost per call: 5 for the interpreter, 2 for baseline, and 0.7 for the optimizer.

The total is the compile cost plus the cost per call times the number of calls. The playground starts at 20 calls.

The cost model at 20 calls. The interpreter costs 100: 0 to compile and 5 per call. The baseline compiler costs 52: 12 to compile and 2 per call. The optimizer costs 94.0: 80 to compile and 0.7 per call.20 callsinterpreter1000 to compile5 per callbaseline5212 to compile2 per calloptimizer94.080 to compile0.7 per call
At 20 calls, the baseline total, 52, is the lowest. Its small compile cost is already paid back, but the optimizer's big compile cost is not.
How many calls make a baseline compiler worth it?
The cost model behind 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 costs the least
Interpreter100.0

Nothing to compile, but every call pays the interpreter's overhead.

compile
0.0
calls
100.0
Baseline compiler52.0

A small compile cost removes the interpreter's overhead, without deep work.

compile
12.0
calls
40.0
Optimizing compiler94.0

A big compile cost only pays back when the function runs very often.

compile
80.0
calls
14.0
Try it yourself

Baseline compiler costs the least for 20 calls in this model: 12.0 to compile plus 40.0 for the calls makes 52.0 in total. Real engines also use notes about values, memory limits, and background threads. This model only shows the shape of the trade-off.

This is a small model with made-up units. It does not look inside your browser's engine or show V8's real limits. Pick a number of calls and compare the totals.

The buttons for 3, 20, and 200 calls are just samples. With these exact costs, baseline first costs less than the interpreter at 5 calls. The optimizer first costs less than baseline at 53 calls. Real engines decide with more facts: call counters, inline caches, notes about types, memory limits, and compiling on a background thread.

Checking the facts in Node

TESTED

You can check some of these facts in Node, because Node uses V8. The lesson tests run Node 22, which comes with V8 12.4. They don't depend on exact version numbers, memory addresses, or timings. They check four things:

  • the Sparkplug options exist,
  • --print-bytecode prints the bytecode shown earlier,
  • %CompileBaseline exists,
  • after %CompileBaseline, V8's status for the function has the kBaseline bit turned on.

Words that start with %, like %CompileBaseline, are V8 native syntax. They are special test commands. They only work when Node starts with the --allow-natives-syntax option, and they are not normal JavaScript.

Ask V8 to compile stamp with Sparkplug (Node only)JavaScript
// 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));

In plain words, line by line:

  1. Line 1 says how to run the file.
  2. Line 2 names two status bits. A status bit is one yes-or-no flag inside a number. kIsFunction is bit 0, and kBaseline is bit 15.
  3. Lines 3 to 6 are the same stamp function as before.
  4. Line 8 asks V8 to compile stamp with Sparkplug right now.
  5. Line 9 prints 42.
  6. Line 10 reads V8's status number for stamp.
  7. Lines 11 and 12 use & to test one bit each. Both print true.

The next commands run the same kind of checks from a terminal.

Terminal commands the tests useShell
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

Line 1 lists V8's options and keeps only the four this lesson needs. Line 2 prints the bytecode for stamp. Line 3 runs a status check with Sparkplug used for every function. Line 4 runs the same check with Sparkplug turned off.

What the tests showed

In Node 22 with V8 12.4, --sparkplug is on by default. The options --always-sparkplug, --no-sparkplug, --print-bytecode, and --trace-baseline-batch-compilation all exist. %CompileBaseline(stamp) turns on the kBaseline bit. These are facts about V8 only, for testing. They are not browser JavaScript.

Using this in real code

PRACTICAL

In day-to-day work, you rarely need to know whether a function runs in Sparkplug or in another engine's baseline tier. What helps is knowing three things:

  • The first few runs of a function can be slower than later runs, because the engine has not compiled it yet. The later runs are called warm runs.
  • A function that runs a medium amount can get faster in baseline code, without ever reaching the optimizer.
  • Objects that keep the same shape, from the last lesson, still help the later tiers.

The table below turns this into advice. Read each row as one situation, what baseline compilers mean for it, and what to do.

What baseline compilers mean for your code
SituationWhat baseline compilers mean hereWhat to do
Short helpers on a pageThey may only ever run in the interpreter or the baseline tier.Make the page start fast and keep the code clear, before you chase top speed.
Helpers that run a medium amountBaseline code can remove the interpreter's overhead without waiting for a strong compiler.Keep object shapes and value types the same, so the engine can see a pattern.
Small timing testsThe first runs can include bytecode, a baseline compile, and a later move up a tier.Run the code a few times first, take several timings, and say exactly what you timed.
Real app codeTier names and limits are inner details of each engine.Don't use V8 native syntax, and don't write code for one compiler.

Measure first. If something a user sees is slow, use a profiler, a tool that shows where the time goes, in the browser or Node process that runs it. Keep the code easy to read. Keep the data in hot code consistent. Use V8 native syntax only for experiments, never in real code.

Common misconceptions

“Baseline code is low quality and useless.”

No. Baseline code can save a lot of time, because it quickly removes the interpreter's overhead.

“Sparkplug optimizes like TurboFan.”

No. Sparkplug is simple on purpose. TurboFan, V8's top compiler, and the other optimizing compilers study the code much more deeply.

“Frames that match the interpreter are a JavaScript rule.”

No. They are a choice V8 made inside its engine. The rules of JavaScript come from the language specification, not from how V8 lays out its frames.

“All engines use Sparkplug.”

No. Sparkplug is V8's name. SpiderMonkey and JavaScriptCore have their own baseline tiers, with their own names and code.

“I should force baseline compilation in real code.”

No. Native syntax needs a special Node option and only works in V8. Measure what users really do instead.

The table below sums up ideas that are easy to mix up. Read each row as an idea, what it means, and what it does not mean.

Ideas that are easy to mix up
IdeaWhat it meansWhat it does not mean
Baseline compilerA quick compiler that makes machine code and removes the interpreter's overhead.The strongest compiler, which studies and rewrites the whole function.
BytecodeThe short list of simple steps that a baseline compiler reads.Machine code that the processor can already run.
Frames that match the interpreterFrame slots laid out so tiers can share the debugger and OSR code.A promise that every engine uses V8's exact slots.
V8 native syntaxTest commands that only work with a special Node option.Normal JavaScript that you can use in an app.

Practice exercises

6 EXERCISES
Exercise 1 · Warm-upRead the first template

What is the first template the toy compiler makes?

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 add in the bytecode

    Which bytecode line adds the argument total to fee?

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

      Exercise 3 · PracticeWork out the baseline cost

      What number does the cost model print for baseline at 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 · PracticeName the status bit

        In the Node code, which V8 status bit shows that stamp was compiled by Sparkplug?

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

          Exercise 5 · PracticeFind the slot that changes

          In the Sparkplug frame model, what sits in the slot where the interpreter kept the bytecode offset?

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

            Exercise 6 · ChallengeApply this to a real app

            A checkout helper feels slow the first time someone clicks. What should you do before you rewrite it for one compiler?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            // A checkout helper feels slow the first time someone clicks.
            // What should you do before you rewrite it for one compiler?

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

              Check your understanding

              Answer these without guessing from the tier names. For a code question, work out what the code prints first. Then link it back to the idea from the lesson.

              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 code 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 choice keeps Sparkplug's compile quick?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does this cost code print?

                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 is it useful that Sparkplug's frames match the interpreter's?

                Choose an answer to see the explanation.

              6. Question 6 of 8Which sentence is true about SpiderMonkey's baseline tiers?

                Choose an answer to see the explanation.

              7. Question 7 of 8In JavaScriptCore's public overview, which tier comes after LLInt?

                Choose an answer to see the explanation.

              8. Question 8 of 8Which rule for real code follows from this lesson?

                Choose an answer to see the explanation.

              Key takeaways

              • Sparkplug is V8's quick baseline compiler. It turns Ignition's bytecode straight into machine code, without building an IR first.
              • Sparkplug's frames have the same slots as the interpreter's frames. So switching tiers, OSR, and the debugger keep working.
              • SpiderMonkey and JavaScriptCore have their own baseline tiers. They do the same job, with different names and inner details.
              • A baseline compiler gives up top speed to compile quickly. Measure real code before you change it for any compiler.
              One-line summary

              A baseline compiler is the quick middle step: it turns bytecode into simple machine code fast, so code skips the interpreter's overhead long before the stronger compilers are worth their time.

              Next, Optimizing compilers shows what engines do when a function runs so often that it is worth spending more time on faster machine code. You'll meet Maglev, TurboFan, Warp, DFG, and FTL.

              CompleteFrontend Clear concepts. Working examples.