V8pedia

On-stack replacement (OSR)

Tiering up normally takes effect on the next call to a function. But what about a function that is currently running a long loop and may not return for seconds? Waiting for the next call is useless. On-stack replacement (OSR) solves this: it transfers a running execution into freshly-optimized code, in the middle of a loop.

::: info Ubiquitous language OSR: replacing the code backing an active stack frame while it runs, typically at a loop back-edge. OSR urgency: a counter that escalates how aggressively V8 wants to OSR a hot loop. :::

The motivating case

function compute() {
  let total = 0;
  for (let i = 0; i < 1e9; i++) total += heavy(i);  // never returns for a while
  return total;
}
compute();

This loop is white-hot, but compute is called once. Pure call-triggered tiering would run the whole billion iterations in the interpreter/baseline before the optimized compute ever gets a chance. OSR exists precisely for this shape.

How it triggers: urgency, not immediacy

OSR does not compile-and-jump the instant a loop is hot. Instead, V8 raises an urgency level stored on the FeedbackVector:

void TrySetOsrUrgency(Isolate* isolate, Tagged<JSFunction> function,
                      int osr_urgency) {

  DCHECK_GE(osr_urgency, fv->osr_urgency());   // never lower urgency here
  fv->set_osr_urgency(osr_urgency);
}

src/execution/tiering-manager.cc#L254-L298

The JumpLoop bytecode at each loop back-edge checks this urgency. When it is high enough, the loop requests OSR compilation; once the optimized OSR code is ready, the next back-edge enters it directly, carrying the loop's live state across. There are separate budgets for OSR into Maglev (invocation_count_for_maglev_osr, default 100) and into TurboFan (invocation_count_for_osr, default 500).

The hard part: entering mid-function

Normal optimized code is entered at the top, with a fresh frame. OSR code is entered in the middle, and must adopt the current frame's live values (locals, the loop counter, accumulator). The compiler builds a special entry that maps the interpreter/baseline frame slots into the optimized frame:

class OsrHelper {
 public:
  void SetupFrame(Frame* frame);            // prepare frame w.r.t. OSR
  size_t UnoptimizedFrameSlots();           // slots to import from the live frame
  static int FirstStackSlotIndex(int parameter_count) {
    return 1 + parameter_count;             // receiver + params
  }
};

src/compiler/osr.h#L23-L42

In effect, OSR is deoptimization run in reverse: deopt rebuilds an unoptimized frame from an optimized one; OSR builds an optimized frame from an unoptimized one. They share the same notion of mapping between frame layouts.

::: details Why "PrepareForOnStackReplacement" is a deopt reason You may have seen PrepareForOnStackReplacement in the deopt reasons. The compiler can insert a controlled bailout at the OSR boundary to set up the transition cleanly — so OSR and deopt are wired through overlapping machinery, which is why they appear together. :::

Why it matters

Without OSR, a single hot loop in a top-level script (think: a benchmark, a parser, a number-crunching routine that runs once but for a long time) would never benefit from optimization. OSR closes that gap, making "hot loop in a cold function" — an extremely common real-world shape — fast.

See also