bi-11 — Analysis
Required invariants
- 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.
- Input has the highest priority, so a flood of timers cannot starve a click.
- Anti-starvation bounds prioritisation. Strict priority without it produces a browser where a busy animation permanently blocks lower-priority work.
- A microtask checkpoint drains exhaustively, including microtasks enqueued during the drain —
so
awaitis not a yield. - Rendering happens between tasks, never inside one. DOM mutations are invisible until the current task ends.
- Policies are per-frame, not per-page. A background iframe can be throttled independently.
The five policies, with their constants
| Policy | Mechanism | Constant |
|---|---|---|
| Priorities | input highest; compositor high during gestures | default normal |
| Pausing | ScopedPagePauser, nested run loop | no JS during alert()/print()/breakpoints |
| Deferring | after a user gesture | 2 seconds |
| Freezing | background pages | 5 minutes on mobile; heuristics on desktop |
| Throttling | JS 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
| Mistake | Consequence |
|---|---|
| Chunk with microtasks | no responsiveness gain; a runaway chain freezes the tab |
setTimeout(f, 0) in a hot loop | 4 ms clamp after nesting depth 5 → ~250 chunks/s ceiling |
| Heavy work in rAF | runs before rendering, so it directly delays the frame |
Yield too often via MessageChannel | continuation goes behind unrelated tasks; your own work starves |
| Benchmark unthrottled on desktop | priority 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.