Web Workers vs scheduler.yield() for CPU Work
Run one CPU fixture through blocking, yielded, and worker lanes, expose transfer and cancellation costs, and export a requirement-driven scheduling receipt.
Web Workers vs scheduler.yield is not a contest for the fastest API; it is a choice about where CPU work belongs. This comparison runs one deterministic workload through blocking, cooperative, and off-thread lanes, verifies equal results, exposes overhead and cancellation, and ends with a conditional scheduling receipt.
Define the Web Workers vs scheduler.yield choice
Web Workers vs scheduler.yield is a question about ownership. Yielding keeps work on the main thread but gives higher-priority tasks opportunities to run between chunks. A dedicated worker moves eligible computation to another thread and communicates through messages. Neither choice makes expensive work free.
Start with the user contract: maximum input delay, total completion time, update cadence, cancellation behavior, data size, API dependencies, and browser support. Then describe the browser CPU work: pure loop, DOM-reading algorithm, incremental parser, canvas renderer, compression job, or mixed workflow. The right boundary follows dependencies more than fashion.
This comparison uses one matched synthetic workload in blocking, yielded, and worker lanes. It records long tasks, progress gaps, message and copy cost, completion time, and cancellation latency. Results are local evidence, not universal browser benchmarks.
The closest scheduling baseline is scheduler.yield versus setTimeout. Use that to understand why a purpose-built yield can resume with favorable priority semantics. The question here is larger: when should cooperative main-thread work become isolated worker work?
| Lane | DOM access | Data crossing |
|---|---|---|
| Blocking | Direct | None |
| scheduler.yield | Direct between slices | None |
| Worker | Unavailable | Clone or transfer |
Reading rule: labels, order, values, shapes, and line styles carry the conclusion; color is supplementary.
Map what must stay on the main thread
The DOM, layout, most UI state, and many framework operations belong to the main thread. A calculation that repeatedly reads layout or mutates elements cannot simply be pasted into a worker. First separate the pure data transform from capture and commit.
Write the task as three stages: gather a bounded snapshot, compute without DOM access, and apply a validated result. If the middle stage dominates cost and inputs serialize cleanly, a worker boundary is plausible. If every small step depends on current UI state, yielding may be simpler and more correct.
The HTML Standard defines workers, their event loops, messaging model, and restricted global scope. That contract makes isolation real: closures, live nodes, and arbitrary application objects do not cross the boundary.
The first figure maps APIs and data across main-thread, yielded, and worker lanes. Text labels mark unavailable dependencies and transfer points. A reviewer can see whether the proposed worker isolates computation or merely adds a message hop around DOM work. Web Workers vs scheduler.yield becomes clearer once the task graph is explicit.
Design a cooperative yielded loop
Break the loop into units small enough to respect the interaction budget. After each slice, inspect cancellation, publish bounded progress, and await scheduler.yield when available. This main thread yielding pattern is long task optimization through cooperation, not parallel execution. Use a fallback that schedules a new task rather than a microtask; an endless promise chain can still starve rendering and input.
Choose slice size from measured duration on target devices, not an iteration count copied from a desktop. A five-millisecond goal might translate to thousands of operations on one machine and hundreds on another. Adapt cautiously and keep an upper bound.
The Scheduling APIs draft specifies scheduler.yield and continuation semantics. Feature-detect it because support can vary. The lab uses the native function when present and a MessageChannel fallback otherwise, labeling which path ran.
Yielding preserves direct access to in-memory state and avoids serialization, but every slice still consumes main-thread time. The responsive scheduler.yield tutorial shows how to keep the continuation contract explicit. Use this lane when work is modest, state coupling is high, and incremental completion is useful.
Build a worker protocol, not a code dump
A worker needs a versioned message protocol: start, progress, result, error, cancel, and disposed. Every job has an ID. The main thread ignores stale messages after cancellation or replacement. The worker checks cancellation between bounded chunks, because terminating the whole worker is not always the desired cleanup policy.
Decide how bytes cross. Structured cloning is convenient but can copy large data. Transferable ArrayBuffers move ownership without copying, leaving the sender’s buffer detached. SharedArrayBuffer requires security headers and a much stricter synchronization design. Measure with the real payload.
Worker startup, module loading, message dispatch, serialization, and result commit are part of the latency ledger. Reuse a healthy worker when tasks repeat, but bound idle lifetime and recover from errors. A worker pool that grows with clicks can trade jank for memory pressure.
Web Workers vs scheduler.yield is not only a throughput comparison. A worker can protect main-thread responsiveness even when total completion becomes slightly slower. That can be the correct product trade. The protocol receipt records data ownership, progress frequency, cancellation, startup policy, and fallback before any numbers are compared.
- Blocking produces one uninterrupted main-thread task.
- Yielded work keeps the same arithmetic but creates continuation points.
- Worker work runs off-thread and pays startup and message edges.
- All lanes must return the same fixture digest before timing is compared.
Reading rule: labels, order, values, shapes, and line styles carry the conclusion; color is supplementary.
Run one workload through all three lanes
Use a deterministic CPU fixture such as scoring a fixed numeric corpus with a repeatable transform. Blocking runs the full loop on the main thread. Yielded runs identical arithmetic in measured slices. Worker runs the same transform and input in a dedicated module, then returns the same digest.
Verify result parity first. If digests differ, performance numbers are irrelevant. Freeze corpus size, seed, algorithm version, browser build, device, power mode, tab visibility, warmup, and sample count. Report medians and the spread instead of selecting the fastest run.
The web.dev guide to optimizing long tasks explains why breaking work gives the browser opportunities to respond. The lab adds a worker lane and makes overhead visible. It records task durations above the declared threshold, progress gaps, completion, and cancellation response.
The second figure is a matched trace with input markers. Blocking delays them, yielding interleaves them, and the worker leaves main-thread markers mostly clear while paying message edges. It explains mechanism without pretending the drawn milliseconds predict another device.
Choose from constraints, not one score
Web Workers vs scheduler.yield becomes a constraint decision here. Prefer blocking only when measured work stays comfortably below the interaction budget and simplicity has real value. Prefer scheduler.yield when the computation needs main-thread state, can be sliced safely, and total CPU demand is moderate. Prefer a worker when the pure computation is sustained, payload transfer is acceptable, and responsiveness matters more than avoiding protocol code.
Reject a worker when the task is mostly DOM access, payloads dominate compute, or required APIs are unavailable in its scope. Reject yielding when each slice remains too long, progress updates create more work, or aggregate CPU keeps the main thread saturated. Sometimes the correct answer is reduce the algorithm or move the job to a server.
Use long animation frame diagnostics to locate actual script and rendering pressure before redesigning the boundary. Long tasks and animation frames describe related but different evidence.
The third figure is a requirement matrix with explicit disqualifiers and a measure-first outcome. Web Workers vs scheduler.yield should end in a conditional decision: for this workload, payload, fleet, and responsiveness target, choose this lane and retain this fallback.
- Block
- Only for work proven below the interaction budget.
- Yield
- For sliceable, state-coupled, moderate computation.
- Worker
- For sustained pure CPU with acceptable transfer cost.
- Server
- For unsuitable or excessive client work.
- Measure
- For weak or incomparable evidence.
Reading rule: labels, order, values, shapes, and line styles carry the conclusion; color is supplementary.
Test cancellation, failures, and fallbacks
Web Workers vs scheduler.yield also diverges under failure. A fast happy path is not enough. Cancel during blocking work and observe that the request cannot be honored until the loop ends. Cancel between yielded slices and record response delay. Cancel a worker job through the protocol, then prove stale progress and results are ignored.
Inject a worker load error, a malformed message, a transferred buffer used twice, an exception inside compute, and a browser without scheduler.yield. Every case needs a visible, bounded outcome. The UI must restore controls and preserve the last valid result rather than spinning forever.
When visual computation is the workload, OffscreenCanvas generative posters shows a related worker boundary. Rendering ownership, input forwarding, and export still need explicit protocols.
Security matters too. The worker source is local, has no dynamic code execution, and makes no network request. Inputs are bounded before allocation. Progress is throttled. Results are validated on receipt. A mutation that trusts any message ID or accepts a mismatched digest must fail. Web Workers vs scheduler.yield is a reliability choice as much as a performance choice.
Export the scheduling receipt
The receipt stores the task contract, fixture digest, device and browser cohort, lane implementation, support detection, slice target, worker protocol version, payload mode, sample rows, parity result, responsiveness evidence, cancellation result, and chosen fallback. JSON supports replay; CSV preserves comparable runs.
Reparse exports and recompute medians, long-task counts, maximum progress gap, and result equality. Mutation fixtures hide copy cost, compare different corpora, drop failed runs, treat modeled events as observed, and recommend a lane that violates a hard constraint. The independent oracle rejects each case.
Run on at least one representative low-power device before shipping. Laboratory desktop results can understate slice duration and exaggerate worker startup differently. Field Core Web Vitals can reveal interaction pain, but they will not isolate this task without local instrumentation and trace context.
Web Workers vs scheduler.yield has no permanent winner. Browser implementations, APIs, payloads, and application state change. Preserve the smallest correct implementation, the evidence that selected it, and a revisit trigger. When the workload crosses the declared threshold, rerun the same fixture before moving the boundary again.
Runnable local artifact — The lab measures one synthetic fixture in the current browser and device; it does not establish universal performance or field Core Web Vitals.
Run one fixture through three execution lanes, verify identical digests, inspect responsiveness and overhead, then export JSON or CSV.