scheduler.yield for Responsive Long Tasks
Slice CPU-heavy browser work around interaction budgets, continuation priority, cancellation, and fallback.
scheduler.yield is useful when a browser task has enough CPU work to delay input but still belongs in the page rather than a worker.
This tutorial finds safe yield points, preserves continuation state and priority, adds cancellation and fallback, and verifies the result with interaction-shaped traces.
scheduler.yield begins with an interaction budget
Take a client-side search index build over a few thousand small records. On a fast laptop it may finish in one burst; on a modest phone it can block typing, scrolling, and assistive technology events long enough to feel broken. Set a slice budget from product evidence, such as preserving input response under a target percentile, rather than picking an arbitrary item count.
Measure task duration by device cohort and capture input events during the work. Main thread responsiveness is the outcome; the number of yields is only an implementation detail.
For scheduler.yield, the working artifact is an interaction-and-task trace. It records device class, work size, slice duration, input timestamp, presentation delay, and long-task entries. I would stop the release when the optimization is justified only by total completion time; that failure means the evidence cannot support this step's claim.
A slow-device typing trace should interrupt an interaction-and-task trace; capture device class, work size, slice duration, input timestamp, presentation delay, and long-task entries. Stop when the optimization is justified only by total completion time, because that outcome breaks the first boundary under test.
The exact implementation vocabulary here includes cooperative scheduling, so the term remains connected to a concrete decision rather than hidden in metadata.
Understand continuation semantics
The Scheduling APIs draft defines scheduler.yield as a way to yield control while arranging a continuation. That continuation behavior differs from sprinkling timers whose priority and ordering can be harder to reason about. Pin browser support and feature-detect the method; the interface still needs a complete fallback.
Cooperative scheduling works only when code returns to the event loop at deliberate boundaries. Awaiting inside a tight synchronous loop does not help unless the awaited operation actually yields, and yielding after the whole computation is ceremonial.
The decision surface for scheduler.yield is a platform-semantics support note. Its compact receipt contains tested engines, feature detection, continuation ordering, fallback, and known gaps. If an await expression is assumed to create responsiveness, the route stays unresolved and returns to design before polish.
Cancellation midway through a slice should challenge a platform-semantics support note; an uninvolved reviewer must recover tested engines, feature detection, continuation ordering, fallback, and known gaps. Hold the next action when an await expression is assumed to create responsiveness.
The primary references for this decision are Scheduling APIs draft, MDN scheduler.yield, and Long Tasks API draft. The scheduling draft defines continuation semantics, MDN captures deployed support, and Long Tasks supplies an observation surface. The page must still choose slice boundaries from its own input latency, cancellation, and result-integrity measurements.
The exact implementation vocabulary here includes main thread responsiveness, so the term remains connected to a concrete decision rather than hidden in metadata.
- Trace: Capture task and input delay.
- Slice: Choose stable time-bounded boundaries.
- Cancel: Prevent stale continuations from publishing.
- Replay: Prove identical output and better tails.
Choose boundaries that preserve invariants
Partition at points where partial state is valid: after one record, one image tile, one AST subtree, or one fixed batch of matrix rows. Keep the accumulator explicit and do not publish half-updated shared structures that another event handler could observe. Long task slicing should make every continuation resumable from a stable cursor.
If one item can exceed the entire budget, split that item further or move the operation off-thread; yielding only between giant items cannot protect input. Treat each slice like a tiny transaction with a defined before and after.
I would review scheduler.yield through a cursor-and-accumulator state contract, not a slide assembled after implementation. The saved evidence is cursor, stable partial result, slice start, slice end, largest item, and invariant check. The explicit rejection rule is simple: yield points cut through a mutation that must be atomic.
Feature detection disabled should exercise a cursor-and-accumulator state contract, with cursor, stable partial result, slice start, slice end, largest item, and invariant check retained for comparison. Reopen the design if yield points cut through a mutation that must be atomic.
The exact implementation vocabulary here includes yield continuation, so the term remains connected to a concrete decision rather than hidden in metadata.
Use time budgets instead of folklore counts
Record a monotonic slice start, process items until the budget is near, commit the stable cursor, then await the yield continuation. Calibrate the budget from traces because item cost varies and reading the clock also has overhead. A fixed 100-item chunk can be tiny for strings and enormous for image filters.
Avoid chasing an exact millisecond with complicated prediction; a simple bounded loop and an oversize-item detector is usually easier to inspect. Yield continuation should prioritize returning to the same task without preventing urgent work from running between slices.
This part of scheduler.yield becomes testable through a time-budgeted processing loop. Preserve budget, measured slice lengths, items per slice, oversize events, continuation count, and completion. Treat the step as failed whenever a fixed item count is copied across unrelated workloads, even when the visual result appears convincing.
An oversize item should force redesign of a time-budgeted processing loop; the fallback receipt is budget, measured slice lengths, items per slice, oversize events, continuation count, and completion. Treat a fixed item count is copied across unrelated workloads as an explicit failed state.
The exact implementation vocabulary here includes long task slicing, so the term remains connected to a concrete decision rather than hidden in metadata.
| Signal | Yield | Worker |
|---|---|---|
| DOM access | Frequent | None |
| State transfer | Shared | Explicit |
| Compute | Bounded | Heavy |
| Parallelism | No | Yes |
Add cancellation and supersession
A new search query can make the old index or filter pass irrelevant. Check an AbortSignal or generation token at each stable boundary and before publishing the result. Cancellation should erase temporary buffers or let them be collected, leave the last complete UI state intact, and report superseded rather than failed.
Do not wait for every stale slice to finish while newer input queues behind it. Stable task identity also prevents an older continuation from overwriting a newer result after both have yielded several times.
For scheduler.yield, the working artifact is a generation-token cancellation model. It records task ID, input digest, abort time, cursor, cleanup result, successor ID, and publish decision. I would stop the release when stale work can commit after a newer request; that failure means the evidence cannot support this step's claim.
A slow-device typing trace should interrupt a generation-token cancellation model; capture task ID, input digest, abort time, cursor, cleanup result, successor ID, and publish decision. Stop when stale work can commit after a newer request, because that outcome breaks the first boundary under test.
Know when to choose a worker
Yielding shares one main thread more politely; it does not create parallel compute or isolate memory-heavy work. Move deterministic CPU work to a Worker when serialization cost is acceptable, DOM access is unnecessary, and long computation would otherwise consume many slices. Canvas rendering may move through OffscreenCanvas.
Keep main-thread scheduling for work that frequently reads page state, is small enough to complete soon, or would cost more to copy than to slice. The decision should include battery, memory, transfer size, and fallback complexity, not only responsiveness.
The decision surface for scheduler.yield is a yield-versus-worker decision matrix. Its compact receipt contains DOM dependency, compute time, transfer bytes, memory peak, cancellation, device result, and chosen owner. If scheduler.yield is treated as a substitute for parallelism, the route stays unresolved and returns to design before polish.
Cancellation midway through a slice should challenge a yield-versus-worker decision matrix; an uninvolved reviewer must recover DOM dependency, compute time, transfer bytes, memory peak, cancellation, device result, and chosen owner. Hold the next action when scheduler.yield is treated as a substitute for parallelism.
- 1Trace
Capture task and input delay.
- 2Slice
Choose stable time-bounded boundaries.
- 3Cancel
Prevent stale continuations from publishing.
- 4Replay
Prove identical output and better tails.
Progress without creating accessibility noise
Update visible progress at a slower semantic cadence than internal slices. A percentage that changes hundreds of times can flood live regions and spend the main-thread budget you recovered. Prefer meaningful milestones, a polite status region, and a cancel control that remains reachable.
Preserve focus and do not move content above the active control on every commit. For indeterminate phases, say what is happening rather than inventing false precision. The responsive version should improve keyboard and screen-reader experience as well as pointer input.
I would review scheduler.yield through a progress announcement schedule, not a slide assembled after implementation. The saved evidence is internal slices, visual updates, announced milestones, focus position, cancel response, and reduced-motion state. The explicit rejection rule is simple: every continuation emits an accessibility announcement.
Feature detection disabled should exercise a progress announcement schedule, with internal slices, visual updates, announced milestones, focus position, cancel response, and reduced-motion state retained for comparison. Reopen the design if every continuation emits an accessibility announcement.
Connect scheduling to UI architecture
Worker boundaries isolate compute, input budgets define the user-visible target, offscreen rendering moves eligible canvas work, and progressive rendering decides when partial results are useful. Use those adjacent patterns to keep the scheduling choice local.
A page should not develop a universal yielding helper that obscures which invariant each task protects. The search index, poster renderer, and data table may share instrumentation while keeping distinct cursors, cancellation rules, and result publication contracts.
This part of scheduler.yield becomes testable through a task ownership map. Preserve task, state owner, yield boundary, worker eligibility, progress policy, and responsiveness budget. Treat the step as failed whenever one generic scheduler hides task-specific correctness, even when the visual result appears convincing.
An oversize item should force redesign of a task ownership map; the fallback receipt is task, state owner, yield boundary, worker eligibility, progress policy, and responsiveness budget. Treat one generic scheduler hides task-specific correctness as an explicit failed state.
Related implementation evidence lives in cancellable fetch pipelines, accessible virtualized feeds, offscreen canvas posters, and scroll-driven storytelling. Virtualization, streaming UX, OffscreenCanvas, and motion guidance solve neighboring pressure but not cooperative main-thread ownership. Keep their performance receipts beside the slice trace so moving work does not merely hide a stale-result bug.
The fixture partitions deterministic item costs into bounded cooperative slices and exposes an oversize item.
Runnable artifact — cooperative-slice.test.mjs
import assert from "node:assert/strict";
const chunks=(items,budget)=>{let slices=1,used=0;for(const cost of items){if(used+cost>budget){slices++;used=0}used+=cost}return slices};
assert.equal(chunks([3,4,5,2,7],8),3);assert.equal(chunks([9],8),2);
console.log("PASS: long task yielded predictably");
Run node cooperative-slice.test.mjs. Expected receipt: PASS: long task yielded predictably.
Verify hostile input during every slice
Replay typing, pointer movement, scrolling, resize, visibility changes, cancellation, navigation, and a slow-device CPU profile while the work runs. Compare total duration, longest task, input presentation delay, slice distribution, memory, and result hash with the unsliced reference. The output must remain identical and the interaction tail must improve.
scheduler.yield is ready when the trace shows urgent work entering between stable continuations without stale publication or unbounded slowdown. Retain the fixture so later data growth cannot silently recreate a monolithic task.
For scheduler.yield, the working artifact is a deterministic interaction replay. It records reference hash, sliced hash, input delays, longest slice, total cost, cancellation result, and browser. I would stop the release when a synthetic loop passes without concurrent user events; that failure means the evidence cannot support this step's claim.
A slow-device typing trace should interrupt a deterministic interaction replay; capture reference hash, sliced hash, input delays, longest slice, total cost, cancellation result, and browser. Stop when a synthetic loop passes without concurrent user events, because that outcome breaks the first boundary under test.
Use scheduler.yield only where stable, bounded continuations improve interaction without changing the computation result; move unsuitable CPU work to a worker. Reopen the slice budget after browser support, data volume, or device-tail regressions change, and reject any revision whose responsive trace produces a different output hash.