cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Speculation & deoptimization

Learn how JavaScript engines optimize for observed types, guard those assumptions, and deopt safely when runtime values change.

By the end, you can
  • 01
    Explain speculative optimizationDescribe how feedback lets an optimizer compile the likely case while keeping a fallback for JavaScript's dynamic behavior.
  • 02
    Read deopt triggersSeparate inline guards and checks from watchpoint-style dependencies, then connect them to eager and lazy deoptimization.
  • 03
    Investigate without overfittingUse --trace-deopt carefully, recognize deopt loops, and turn the lesson into stable, measured production habits.

Optimize the likely case, keep an exit

JavaScript engines want hot code to run like carefully planned machine code, but JavaScript values can change shape and type at runtime. The compromise is speculation: compile the case that has happened so far, then keep a safe exit for the case that happens next.

Definition

Speculative optimization means an engine compiles optimized code using runtime feedback, guards the assumptions it made, and deoptimizes when an assumption stops being true. Deoptimization reconstructs a less-specialized frame so JavaScript still produces the correct result.

This lesson builds on type feedback and inline caches. Those lessons explain how engines collect evidence; here you use that evidence to understand why optimized code needs guards, watchpoints, and deopt metadata. The previous lesson, Optimizing compilers, explains the compilers that emit the optimized code.

Real-life analogyFavorite snacks on the top shelf

Keep your favorite snack on the top shelf because you choose it often. When a guest asks for something different, go back to the recipe instead of forcing the usual choice.

In real life: You often pick the same snack
In JavaScript: Feedback says the hot path has familiar inputs
In real life: You keep it on the top shelf
In JavaScript: The optimizer emits a fast path for the likely case
In real life: A guest asks for something different
In JavaScript: A guard fails because the value no longer matches the speculation
In real life: You go back to the recipe
In JavaScript: The engine deopts to a general path and keeps the JavaScript result correct

Where the analogy stops: A kitchen can choose any shelf. A JavaScript engine must accept valid JavaScript and compute the right answer.

Speculative optimization

A speculative plan has three parts. First, feedback says what values arrived at one site. Second, optimized code uses that pattern. Third, guards check that the next value still fits. A guard is a runtime test that protects an optimization.

Speculation walkthrough: fast path, guard, bailout
Step 0 of 6Ready
Your turn: follow the blue line

Replay a teaching model of speculative optimization: feedback creates a guarded fast path, and a surprising type bails out safely.

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
observe(feedback, 10, 2);const optimized = compileWithGuards(feedback);optimized(12, 3);optimized("12", "3");
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.

In the replay, line 2 warms the add site with numbers. Line 3 compiles a guarded plan. Line 4 passes the guard, but line 5 sends strings. The optimized path bails out, the generic path concatenates, and the result is still "123".

Stable numeric inputs keep one meaning for +Pop out in the code editor (opens in a new tab)JavaScript
function totalCents(price, tax) {  return price + tax;} console.log(totalCents(100, 8));console.log(totalCents(250, 20));

Line 2 always performs numeric addition. The lesson is not “never use strings.” It is “avoid making one hot operation alternate between two meanings unless that is the behavior you really need.”

The feedback lattice

A lattice is an ordered set of states where new evidence can move a site toward a more general state. For the binary add operation, V8 12.4 names hints such as None, SignedSmall, Number, NumberOrOddball, and Any. The exact compiler may split more cases, but the safe teaching model is that a site widens when broader values appear.

A simplified BinaryOp feedback lattice for addition
StateWhat it has seenWhat an optimizer may try
NoneNo useful evidence yet.The interpreter collects feedback before optimized code should trust this site.
SignedSmallSmall integers, called Smis in V8, have been enough so far.The optimizer can try tagged-integer math with overflow and Smi checks.
NumberAt least one non-integer number appeared.The plan can still be numeric, but it cannot assume integer-only arithmetic.
NumberOrOddballNumbers mixed with undefined, null, or booleans.The path must include JavaScript conversion behavior.
AnyThe site mixed incompatible meanings such as number addition and string concatenation.Use the generic operation unless a later compiler splits control flow more precisely.
Verified source fact

V8's BinaryOperationHint enum in src/objects/type-hints.h includes kNone, kSignedSmall, kNumber, kNumberOrOddball, kString, kBigInt, kBigInt64, and kAny. The test also asks Node 22's V8 to print real feedback for small integers, numbers, and oddballs.

Node-only proof of BinaryOp feedback namesJavaScript
function onlySmi(a, b) { return a + b; }for (let i = 0; i < 30; i += 1) onlySmi(i, i + 1);console.log("ONLY_SMI");%DebugPrint(onlySmi); function smiThenDouble(a, b) { return a + b; }for (let i = 0; i < 30; i += 1) smiThenDouble(i, i + 1);smiThenDouble(1.5, 2.25);console.log("SMI_DOUBLE");%DebugPrint(smiThenDouble); function numberOddball(a, b) { return a + b; }for (let i = 0; i < 30; i += 1) numberOddball(undefined, i);console.log("ODDBALL");%DebugPrint(numberOddball);
Play with one speculative add site
Teaching-model lattice inputJavaScript
const slot = createSlot("BinaryOp slot #0");feed(slot, 1, 2);          // SignedSmallfeed(slot, 1.5, 2.25);     // Numberfeed(slot, undefined, 1);  // NumberOrOddballfeed(slot, "7", "8");     // Any
Current planno optimized plan yet
None feedback state0 deoptsno throttled

No calls yet. Start with small integers, then try strings or `undefined`.

Step 0 of 7None

Feed values into one + site. The state starts at None, widens as needed, and the teaching model disables optimization after repeated guard failures.

This is a teaching model; it does not inspect your browser's engine. It computes real JavaScript results while modeling the feedback and deopt decisions described in the lesson.

Try the scripted sequence, then reset and feed strings first. A fresh string-only site can specialize differently, but mixing number addition and string concatenation at the same site pushes this model to Any.

Guards, checks, and watchpoints

Inline checks protect facts about values in the current optimized frame. A Smi check protects small-integer math. A map check protects an object layout. A bounds check protects an array access. A watchpoint, also called a code dependency in V8 traces, protects a fact that can be invalidated from elsewhere, such as a stable prototype or constant field.

Checks and dependencies that keep speculation safe
MechanismWhere it livesPlain-language testLikely fallback
Smi checkInline guard in optimized codeif input is not a small integer, deopt nowEager deopt
Map checkInline guard for an object shape/mapif receiver map changed, deopt nowEager deopt
Bounds or hole checkInline guard around arraysif index is out of bounds or the elements kind changed, use the fallbackUsually eager deopt or a slower builtin
Stable-map dependencyWatchpoint stored outside this exact instructionif that map/prototype becomes unstable, mark dependent code staleLazy invalidation
Constant field dependencyWatchpoint for a property/prototype assumptionif someone writes the field, invalidate compiled codeLazy invalidation
Real-life analogyChecking a classroom list

A teacher can check one name at the door. If the whole class list changes, later checks need the new list. Both protect a shortcut, but they act at different times.

In real life: A teacher checks one name now
In JavaScript: An inline guard checks this value right now
In real life: A missing name needs an immediate check
In JavaScript: A guard failure causes eager deopt
In real life: A changed class list affects many checks
In JavaScript: A watchpoint invalidates code that depended on a wider fact
In real life: The next check uses the new list
In JavaScript: The next safe entry or return takes the lazy fallback

Where the analogy stops: A class list is visible to people. Engine dependencies are internal metadata, and engines implement them differently.

Eager vs lazy deoptimization

Eager deoptimization happens at the failing check. The optimized frame cannot continue because the current value violates a guard. Lazy deoptimization is delayed until a safe point after optimized code was marked invalid by a dependency or watchpoint. Both paths need metadata that maps optimized machine state back to interpreter-level variables.

Watchpoint walkthrough: invalidate now, fall back later
Step 0 of 6Ready
Your turn: follow the blue line

Replay a teaching model of a dependency invalidation: optimized code is marked stale from the outside, then falls back on the next safe entry.

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
const plan = compilePriceReader(menu);plan({ subtotal: 100 });changeTaxRate(menu, 0.12);plan({ subtotal: 100 });
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.
Eager and lazy deoptimization compared
KindTriggerTrace clueExamples
Eager deoptA guard/check in the optimized frame fails right now.V8's trace line contains bailout (kind: deopt-eager, reason: ...).Wrong type, wrong map, out-of-bounds, insufficient feedback.
Lazy deopt or dependency invalidationSome outside fact invalidates code that may not be running at the exact site.V8 can mark dependent code for deoptimization because of code dependencies.Prototype changes, stable maps, constant fields, protector cells.
OSR-related deoptA loop entered optimized code mid-frame and later exits that optimized path.The on-stack replacement lesson covers the loop-entry half in detail.Long loops and early exits from OSR code.
Sort the speculation and deopt clues
  • A hot price + tax site only saw small integers, so the compiler emits a Smi fast path.
  • The same optimized add site receives "12" and the Smi guard fails.
  • A prototype property changes and V8 marks dependent optimized code for deoptimization.
  • Convert API prices with Number(value) once before a hot reducer runs.
  • A property load compiled for one map receives an object with a different map.
  • Use a benchmark with warm-up and representative values before rewriting a helper.
Try it yourself
0 of 6 correct

Place each card with the idea it best demonstrates: speculation, eager deopt, lazy invalidation, or a practical fix.

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

The watchpoint replay is a teaching model. It does not inspect your browser engine. It shows why code can be invalidated by a change outside the exact instruction that later falls back.

Deopt loops

A deopt loop is a pattern where code optimizes for one case, quickly sees another case, deopts, gathers feedback, and then repeats. Engines track this kind of churn and can delay or stop re-optimizing. Your practical fix is usually simpler: keep the hot operation's inputs stable, or split truly different cases into different paths.

One source site with two meanings for +Pop out in the code editor (opens in a new tab)JavaScript
function addOne(value) {  return value + 1;} const values = [1, "1", 2, "2"];console.log(values.map(addOne).join("|"));

Line 6 calls the same addOne site with numbers and strings. JavaScript prints 2|11|3|21. A real optimizer may learn from that, but if the hot path keeps alternating, repeated specialization attempts can become wasted work.

  • Normalize API data once, before it enters a hot loop.
  • Keep one hot call site from mixing many unrelated shapes or type meanings.
  • Split rare cases out when it improves clarity and measurements say the path matters.
  • Prefer readable code; do not contort code for a trace line that is not on a hot path.

Tracing deopts with Node

V8 exposes diagnostic flags in Node. These examples are marked non-runnable in the browser editor because they require Node flags and V8 native syntax. The tests run them in child processes and assert stable facts only: Node starts with v22., V8 starts with 12.4., eager deopt traces include a kind and reason, and dependency invalidation traces mention code dependencies.

Node-only eager deopt probeJavaScript
function add(a, b) {  return a + b;} %PrepareFunctionForOptimization(add);for (let i = 0; i < 100000; i += 1) {  add(i, i + 1);}%OptimizeFunctionOnNextCall(add);add(1, 2);add("x", "y");
Stable shape of the eager traceShell
$ node --allow-natives-syntax --no-concurrent-recompilation --trace-deopt probe.js
[bailout (kind: deopt-eager, reason: not a Smi): begin. deoptimizing ...]
Node-only dependency invalidation probeJavaScript
const proto = { taxRate: 0.05 };const menu = Object.create(proto);menu.subtotal = 100;function price(row) {  return row.subtotal * (1 + row.taxRate);}%PrepareFunctionForOptimization(price);for (let i = 0; i < 100000; i += 1) price(menu);%OptimizeFunctionOnNextCall(price);price(menu);proto.taxRate = 0.12;price(menu);
Stable shape of dependency invalidation outputShell
$ node --allow-natives-syntax --trace-deopt probe.js
[marking dependent code ... for deoptimization, reason: code dependencies]
Node-only optimization status probeJavaScript
function add(a, b) { return a + b; }%PrepareFunctionForOptimization(add);add(1, 2);%OptimizeFunctionOnNextCall(add);add(3, 4);const status = %GetOptimizationStatus(add);console.log("status is number", typeof status === "number");
What not to assert

Do not assert memory addresses, compile timings, bytecode offsets, exact optimization status numbers, or full trace lines. They vary across platforms and minor builds. Prefer broad evidence such as kind: deopt-eager and the presence of a reason.

Practical habits

Most frontend developers should not chase every deopt. Use this knowledge when a measured hot path matters: a parser, a chart reducer, a data-grid formatter, or an animation loop. Then make the values boring in the best way.

Production habits that help speculation without overfitting
HabitWhat to doWhy it helps
Normalize at boundariesConvert API strings to numbers before the hot loop.A hot add site keeps one meaning instead of alternating between addition and concatenation.
Keep object shapes steadyInitialize properties in the same order and avoid delete on hot objects.Inline caches and map checks stay predictable.
Measure before changing codeUse real user paths or representative benchmarks with warm-up.A deopt trace is a clue, not proof that users are slow.
Use diagnostics locallyRun Node or Chrome with trace flags in experiments, not in application code.Flags and native syntax are V8-specific and can change.
Normalize data before the hot reducerPop out in the code editor (opens in a new tab)JavaScript
function toCents(value) {  return Number(value);}const inputs = ["100", "50", "25"];console.log(inputs.map(toCents).reduce((sum, value) => sum + value, 0));

Line 1 converts each API value once. Line 5 then reduces numbers, not a mixture of strings and numbers. Measure before and after in the application path you care about.

Common misconceptions

  • “Deopt means my code is wrong.” No. It means an optimized assumption stopped matching; JavaScript still runs correctly.
  • “The feedback lattice narrows after a clean call.” Usually no. Feedback tends to widen so the next plan stays safe.
  • “Watchpoints run my JavaScript callback.” No. They are engine dependencies that invalidate compiled code internally.
  • “Every trace line must be fixed.” No. Fix measured hot paths, not diagnostic noise.
Similar ideas that are easy to mix up
IdeaActually meansNot the same as
SpeculationAn implementation strategy backed by runtime feedback and guarded exits.A change to JavaScript semantics or a guess that may return the wrong answer.
Type feedbackEvidence collected at operation sites.A guarantee that future values must have the same type.
DeoptimizationA correctness-preserving fallback to a less-specialized frame.An exception or a bug in your program.
Trace outputA diagnostic clue for one engine build.A portable performance contract or something to assert byte-for-byte.

Practice exercises

Exercise 1 · Warm-upPredict the stable numeric output

Run the code mentally. What is the first printed line?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function totalCents(price, tax) {
  return price + tax;
}

console.log(totalCents(100, 8));
console.log(totalCents(250, 20));

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

    Exercise 2 · Warm-upName the widened lattice state

    Use the simplified lattice table. Which state is last?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    let state = "None";
    function merge(previous, observed) {
      if (previous === "None") return observed;
      if (previous === observed) return previous;
      if (previous === "SignedSmall" && observed === "Number") return "Number";
      if (previous === "Number" && observed === "NumberOrOddball") return "NumberOrOddball";
      return "Any";
    }
    state = merge(state, "SignedSmall");
    state = merge(state, "Number");
    state = merge(state, "NumberOrOddball");
    console.log(state);

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

      Exercise 3 · PracticeFind the mixed-input surprise

      What does the second call print, and why might that surprise a numeric fast path?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function addFee(value) {
        return value + 1;
      }
      
      console.log(addFee(4));
      console.log(addFee("4"));

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

        Exercise 4 · PracticeWrite a boundary normalization helper

        Complete the helper so a later reducer sees numbers, not a mix of strings and numbers.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function normalizePrice(value) {
          // return a number here
        }
        
        const raw = ["100", "50", "25"];

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

          Exercise 5 · PracticeRead a deopt trace kind

          Which substring in the sample output identifies the eager deopt kind?

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

            Exercise 6 · ChallengeApply it to a real site path

            Choose a data-grid, chart, parser, or animation path in an app. What would you inspect before changing code, and what is one safe fix if values alternate?

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

              Quiz

              Speculation and deoptimization quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is speculative optimization?

                Choose an answer to see the explanation.

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

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function addFee(value) {
                  return value + 1;
                }
                console.log(addFee(4));
                console.log(addFee("4"));

                Choose an answer to see the explanation.

              3. Question 3 of 8Which BinaryOp feedback path is the lesson's simplified numeric climb?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does an inline Smi check usually do when it fails in optimized code?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this lattice model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                let state = "None";
                function merge(previous, observed) {
                  if (previous === "None") return observed;
                  if (previous === observed) return previous;
                  if (previous === "SignedSmall" && observed === "Number") return "Number";
                  return "Any";
                }
                state = merge(state, "SignedSmall");
                state = merge(state, "Number");
                state = merge(state, "String");
                console.log(state);

                Choose an answer to see the explanation.

              6. Question 6 of 8What does a watchpoint or code dependency do?

                Choose an answer to see the explanation.

              7. Question 7 of 8What is a deopt loop?

                Choose an answer to see the explanation.

              8. Question 8 of 8Which production response is best after seeing --trace-deopt output?

                Choose an answer to see the explanation.

              Key takeaways

              • Optimized JavaScript is often speculative: fast for the observed case, guarded for correctness.
              • Feedback usually widens through a lattice. One surprising value can make a site more general.
              • Inline guards tend to cause eager deopts; watchpoints and code dependencies can invalidate code lazily.
              • Trace deopts to investigate measured hot paths, then keep types and shapes stable where it improves real work.

              One-line summary: speculation makes the common path fast, and deoptimization keeps the uncommon path correct.

              Next, What optimizing compilers do shows the classic transformations, such as inlining, constant folding, and bounds-check elimination, that speculation makes possible.

              CompleteFrontend Clear concepts. Working examples.