JavaScript Scheduler API Table

PieceWhat it doesField note
await scheduler.yield()Yield, priority intactThe modern chunking primitive - continues BEFORE other tasks
vs setTimeout(chunk)Priority taxsetTimeout drops you to the macrotask queue - input waits behind you
isInputPending()Check before yieldingSkip the yield when the user is mid-interaction
scheduler.postTask(priority)Schedule with priorityuser-blocking / user-visible / background tiers
Long task = 50msThe frame budgetEverything above it blocks input - chunk under it
Continuation semanticsWhere you resumeyield resumes in the same task group - no priority demotion
Chrome-firstThe support gateFallback: the setTimeout chunking pattern (same shape, worse priority)
Chunking patternThe loop shapefor chunk: work(chunk); if (over budget) await yield() - the shared skeleton
Reference: the MDN scheduler.yield reference. scheduler.yield() is the modern long-task fix: await it mid-loop and the browser pauses YOUR work to serve input and rendering first - then resumes you at your PRIORITY, before the macrotask queue. The old setTimeout-chunking worked but demoted you behind every input event. Bottom line: the chunking skeleton is shared (work a chunk, yield when over the 50ms budget), isInputPending() skips yields mid-interaction, and postTask's priority tiers schedule background work around user work. Chrome-first - the setTimeout fallback is the same loop shape. Related tools: event loop table (the queues yield navigates), workers table (heavy compute leaves the thread entirely), and performance table (long tasks are the metric).

scheduler.yield() is the modern long-task fix: await it mid-loop and the browser pauses YOUR work to serve input and rendering first - then resumes you AT YOUR PRIORITY, before the macrotask queue. The old setTimeout-chunking worked but demoted you behind every input event.

Bottom line: the chunking skeleton is shared across eras - work a chunk, check the budget (long task = 50ms), yield when over - and isInputPending() skips the yield entirely when the user is mid-interaction. postTask's priority tiers (user-blocking, user-visible, background) schedule work around user work.

The honest part: scheduler.yield() is Chrome-first - the fallback is the setTimeout chunking pattern, the same loop shape with worse priority behavior. And yielding fixes INTERLEAVING; it does not make the work cheaper. A job that needs the CPU for 5 total seconds takes 5 seconds - yield decides who waits, not whether.

How to use

  1. Chunk the loop: for (const item of items) { work(item); if (performance.now() - start > 40) { await scheduler.yield(); start = performance.now(); } } - under the 50ms task budget.
  2. Skip yields mid-interaction: if (!navigator.scheduling?.isInputPending()) await scheduler.yield() - don't pause while the user is actively typing or dragging.
  3. Tier background work: scheduler.postTask(fn, { priority: 'background' }) - analytics and prefetch yield to everything user-visible.

Frequently asked questions

What problem does scheduler.yield() solve that setTimeout chunking did not?

Priority demotion. The chunking pattern (process a slice, then setTimeout(processNext, 0)) works - but setTimeout schedules you into the MACROTASK queue at default priority: while your chunks continue, every pending input event, rendering opportunity and other task interleaves, and your work stretches unpredictably under load. scheduler.yield() continues your work at its ORIGINAL priority - input and rendering jump ahead (that is the yield), but your continuation is not dumped behind unrelated macrotasks. The result: the same cooperative slicing with stable, predictable completion and no input latency spikes - the intent of chunking, without the tax.

What is the 50ms long-task budget and how do chunks relate to it?

It is the interaction ceiling. Research and the INP metric converge on: tasks longer than ~50ms make the page feel sluggish, because input events arriving mid-task wait for the task to finish before handlers run. So the chunking rule is budget-driven: check elapsed time every iteration or two, and yield before crossing ~40-50ms, leaving the rest of the frame for the browser. Chunks smaller than that are invisible to users; chunks larger become long tasks that show up in the performance panel and in INP. The performance API's longtask entries are the before-and-after proof: the same total work, restructured from one 900ms task into twenty 40ms tasks.

How does isInputPending change the chunking loop?

It makes yields CONDITIONAL. Yielding costs a little (the continuation lands in a later task), so yielding 500 times during a burst of typing is counterproductive - each yield queues behind the input events it was meant to prioritize. isInputPending() reports whether input events are WAITING right now; the refined loop yields only when nothing is pending and yields immediately (or soon) when something is. It is a heuristic, not a guarantee - not all input is reported - but as a yield-skip gate it removes the majority of pointless yields during interactive use while keeping the safety valve for the quiet-render case.

When should heavy work use a Worker instead of yielding?

When the work is pure computation with no DOM dependency. Yielding keeps the work on the MAIN thread - it interleaves politely but still consumes main-thread CPU, and the total wall time is unchanged. A Worker moves the compute OFF the thread: the page stays fully responsive because the work literally runs elsewhere, at the cost of message-passing (structured clone, transferables) and no DOM access. The decision line: interactive-friendly slicing of DOM-adjacent work (render lists, apply styles, parse into state) = yield; heavy pure computation (parsing megabytes, image processing, crypto) = worker, with scheduler.yield as the fallback for work that cannot move. Both beat the naive synchronous loop; they answer different constraints.

Related tools