bi-11 — Analysis

Required invariants

  1. There are multiple task queues, and the implementation chooses. No global FIFO. Code that depends on cross-source ordering depends on something no specification promises.
  2. Input has the highest priority, so a flood of timers cannot starve a click.
  3. Anti-starvation bounds prioritisation. Strict priority without it produces a browser where a busy animation permanently blocks lower-priority work.
  4. A microtask checkpoint drains exhaustively, including microtasks enqueued during the drain — so await is not a yield.
  5. Rendering happens between tasks, never inside one. DOM mutations are invisible until the current task ends.
  6. Policies are per-frame, not per-page. A background iframe can be throttled independently.

The five policies, with their constants

PolicyMechanismConstant
Prioritiesinput highest; compositor high during gesturesdefault normal
PausingScopedPagePauser, nested run loopno JS during alert()/print()/breakpoints
Deferringafter a user gesture2 seconds
Freezingbackground pages5 minutes on mobile; heuristics on desktop
ThrottlingJS timers only at present

"Background tabs are throttled" is folklore. The table is a model you can predict with, and the gap between the two is the gap this module closes.

Failure modes

MistakeConsequence
Chunk with microtasksno responsiveness gain; a runaway chain freezes the tab
setTimeout(f, 0) in a hot loop4 ms clamp after nesting depth 5 → ~250 chunks/s ceiling
Heavy work in rAFruns before rendering, so it directly delays the frame
Yield too often via MessageChannelcontinuation goes behind unrelated tasks; your own work starves
Benchmark unthrottled on desktoppriority effects only appear under load

Why scheduler.yield() exists

With MessageChannel yielding, your continuation is appended to the back of the queue, behind anything that arrived while you worked. Yield often enough and you starve yourself. scheduler.yield() resumes with continuation priority, ahead of newly-arrived same-priority work. That is the whole reason for the API, and it is why the naive advice "yield more often" has a cost curve that turns upward.

INP decomposes into three terms, two of which are not you

INP = input delay + processing + presentation

The most common real profile is a large input delay caused by a long task that was already running. The fix is not optimising the handler — it is not having the long task. LoAF's scripts[] attribution is how you find which one.