Back to Knowledge Hub
Platform

scheduler.postTask Priorities for Responsive Browser Work

Choose user-blocking, user-visible, or background task priorities with scheduler.postTask and verify fallbacks without overstating browser control.

BotBrowser Team

Documentation

Want the structured docs for Platform?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

The Prioritized Task Scheduling API lets an owned page label work as user-blocking, user-visible, or background. BotBrowser supports replaying an authorized fixture and recording visible completion, ordering, and fallback states; it does not control an operating-system scheduler or promise that a third-party site observes one queue order. Use performance timing measurement and performance optimization when the task result must be tied to a visible application decision.

TL;DR

  • Use user-blocking only for work that keeps the current interaction usable, not as a universal speed switch.
  • Use user-visible for work needed for the next render and background for deferrable maintenance.
  • Keep an AbortSignal, a bounded fallback, and an explicit outcome for every posted task.
  • Compare task state and visible results; a priority label is a scheduling hint, not a latency guarantee.

Contents

A task queue routes user-blocking, user-visible, and background work through scheduler.postTask with cancellation and a visible result.

Choose a priority

The WICG Scheduling APIs proposal defines scheduler.postTask() as a way to enqueue a callback with an optional priority and signal. user-blocking is the strongest hint and fits a short action that directly keeps input or feedback responsive. user-visible is the default for work that supports the next visible state. background is for maintenance that can wait, such as cache cleanup or stale preview preparation.

Priority is not a promise about execution time, fairness, or paint order. The browser may yield, pause a document, coalesce work, or reject a task before it runs. Keep the decision tied to a user-visible outcome: a filter result can be user-visible, while an analytics batch belongs in background. Do not mark a whole rendering pipeline user-blocking simply because one button started it.

The MDN Scheduler.postTask() reference documents the accepted values and the TaskController integration. Check the API at runtime and record the browser version when a support decision matters. A supported method still does not make a long callback harmless; split expensive work and yield between chunks.

Post and cancel work

Give each task a small owner record before posting it. The record should contain taskId, priority, fixtureId, startedAt, finishedAt, outcome, and cancelReason; it should not contain page text, account identifiers, or a full URL. The identifier links one visible result to one queue decision without becoming a long-lived user profile.

const controller = new TaskController({ priority: 'user-visible' });
const record = { taskId: 'results-17', priority: 'user-visible', outcome: 'queued' };

const task = scheduler.postTask(
  () => {
    record.outcome = 'rendered';
    renderResults();
  },
  { signal: controller.signal }
);

cancelButton.addEventListener('click', () => {
  controller.abort('superseded');
  record.outcome = 'cancelled';
  record.cancelReason = 'superseded';
});

task.catch(error => {
  record.outcome = error.name === 'AbortError' ? 'cancelled' : 'failed';
});

The TaskController owns cancellation for this task only. Abort before execution should produce an explicit cancelled result; it must not be counted as a slow success. A callback that throws should become failed after the application preserves a usable error state. Keep the catch path attached, because an unhandled rejected task can obscure the original user action.

The controller does not interrupt JavaScript that is already executing. A long callback still blocks the main thread until it yields or returns. For a multi-item render, process a bounded slice, schedule the next slice, and re-check whether the run is still current. Chrome's prioritized task scheduling article describes this cooperative model; it does not turn postTask into a preemptive thread.

Build a fallback

Feature-detect globalThis.scheduler?.postTask before choosing the preferred path. The fallback should preserve the same visible result and cancellation rule with an ordinary queued callback, setTimeout, or the application's existing scheduler. It should not silently promote background work into a blocking action.

ConditionPreferred pathFallback and expected result
API availablepostTask(callback, { priority: 'user-visible' })Record queued; finish with the same visible result
API missingNo scheduler objectQueue one bounded callback and keep the action usable
Signal abortedAbortError before callbackMark cancelled; do not render stale data
Callback throwsRejected task promiseMark failed and show the existing retry action
Work is obsoleteCurrent taskId no longer matchesDrop the result and retain the newer state

The fallback is an application contract, not evidence that two browsers share the same event-loop timing. Preserve the priority as metadata even when the implementation cannot honor it. That makes a later comparison honest: the record can say priority: background and implementation: timeout instead of implying native scheduling.

Avoid a fallback that runs every queued callback in one unbounded loop. Large batches recreate the long-task problem that the API is meant to help manage. The web.dev long-task guidance recommends breaking work into smaller pieces and measuring the user-visible effect. A bounded slice plus a stop condition is easier to cancel and to explain in a support ticket.

Verify ordering without overclaiming

Use a synthetic fixture with three tasks and one controlled cancellation. Post input-feedback at user-blocking, results-render at user-visible, and cache-refresh at background; then record the first visible state and the final outcome for each. The useful assertion is that the current interaction remains usable and obsolete work does not overwrite it. Do not assert a universal order when the browser can yield or a document can be hidden.

Repeat the fixture with the API available, with the API removed, and with AbortController cancelled before execution. Keep browserVersion, viewport, fixtureId, and implementation beside the result. Compare the first changed field rather than collecting an unrestricted trace. If implementation changes, inspect feature detection; if outcome changes, inspect cancellation and ownership; if a visible frame changes, inspect the callback's work size.

When the callback creates worker work, hardware concurrency and worker planning explains why navigator.hardwareConcurrency is a capacity hint rather than a queue guarantee; keep that host decision separate from priority.

Use browser performance measurement with User Timing for the owned interval around this fixture, then keep its duration separate from the scheduler priority outcome.

BotBrowser can load this declared fixture in an authorized browser context and record the visible state labels. It cannot force a third-party event loop, prove that a task ran before a paint, or guarantee that a browser build implements the proposal. A controlled replay is evidence about the fixture and setup, not a claim about every device or site.

Start the review with a question that names the owner and the visible decision. “Does the search result remain usable while stale suggestions are discarded?” is a scheduling question because it identifies the interaction, the obsolete work, and the acceptable outcome. “Which priority is fastest?” is too broad: it hides the callback size, document visibility, input mode, cache state, and browser release. Write the first question into reviewQuestion, then choose the smallest fixture that can answer it.

Keep the queue fixture deterministic in state, even though elapsed time remains an observation. Use synthetic data with 3 result rows, one cancelled request, and one replacement taskId. Freeze the fixture revision and the expected state transitions before changing the priority. A result such as rendered means that the application committed its declared output; it does not mean a person saw every pixel or that the callback finished before the next frame.

The state machine should distinguish queued, running, rendered, cancelled, failed, and superseded. queued means the task was accepted by the chosen implementation. running is useful only when the callback owns a bounded start marker. rendered requires a visible application assertion, while failed requires a captured error class without copying page data. superseded means a newer run owns the same UI contract and the old result must be ignored.

Use one owner for admission, one owner for the callback, and one owner for the visible record. The component that creates TaskController should be able to abort it during teardown. The callback should verify its taskId immediately before committing output. The record writer should accept only the fields in its allowlist. This separation prevents a stale promise from changing the result after navigation and keeps queue telemetry smaller than an application trace.

Visibility changes deserve a separate row. A hidden document may receive less execution time, and a background task that was harmless while visible can become unnecessary after a route change. Listen for the application's lifecycle signal, abort work that no longer has a consumer, and mark the result cancelled with cancelReason: hidden or route-change. Do not reinterpret a paused task as a slow browser; the document state is part of the observation context.

Input events can create a burst of equivalent tasks. Coalesce by a stable requestKey, but never merge two tasks that have different authorization or visible ownership. When a new search term arrives, mark the earlier taskId superseded before enqueueing the replacement. The replacement may use the same priority, yet it is a different attempt with a different expected result. This ordering makes a support record explainable when network responses return out of order.

For long work, measure chunks rather than pretending that one priority controls the entire operation. A background index rebuild can process 20 records, yield, and continue only when its controller is still live. A user-visible list can render 10 rows, commit a loading state, and schedule the next slice. If a chunk exceeds the application's budget, record failed or deferred according to the product contract; never silently relabel the work as user-blocking to hide the delay.

The fallback should expose the implementation name in the record. Use native-postTask when the feature-detected API accepted the callback, timeout when an ordinary timer queued it, and inline only for a deliberately synchronous path. These labels are not browser fingerprints. They explain why two runs have different queue behavior and let a release reviewer separate a capability change from an application change.

Error handling needs two layers. The task promise reports scheduling or callback failure, while the application state reports what the user can do next. A rejected promise with AbortError is a controlled cancellation; a different exception should preserve the retry action and stop the stale result. If the fallback throws before returning a promise, wrap it in the same record path so the native and fallback branches cannot silently diverge.

Do not use priority as an entitlement or security boundary. A user-blocking task can still read only the data its component already owns, and a background task does not make sensitive data safe to retain. Keep credentials, page content, and route tokens out of the queue record. If a job needs a server lease, that lease belongs to the service that owns admission; scheduler.postTask only orders browser work after the page has accepted it.

Compare releases with the same browser family, fixture revision, viewport, input mode, and visibility state. A change from user-visible to background is a product decision, not a neutral tuning operation. Record the reason, expected visible effect, and rollback trigger beside the code change. If the first changed field is implementation, inspect feature detection; if it is cancelReason, inspect ownership; if it is outcome, inspect the visible assertion before collecting another trace.

An audit row can be small and still support a useful decision:

{
  "fixtureId": "queue-01",
  "taskId": "results-17",
  "priority": "user-visible",
  "implementation": "native-postTask",
  "outcome": "rendered",
  "cancelReason": null,
  "visibleState": "results-ready",
  "browserVersion": "155.0",
  "viewport": "1280x800"
}

The JSON is a review shape, not a promise that a browser exposes every field. Keep browserVersion and viewport because they define the observation context, not because they identify a person. Replace visibleState with a product-owned state label rather than a screenshot token or page excerpt. Retain the row only for the approved review window, then aggregate or delete it according to the application's data policy.

The record also gives a reviewer a compact handoff when a queue change reaches production. Include the fixture revision, the expected visible state, the implementation label, and the first field that diverged. Exclude stack traces that contain route parameters, callback arguments, or copied response bodies. A short record is easier to compare across browserVersion values and less likely to become an accidental page-history store.

When a test fails, stop at the first meaningful difference. If the fixture never reaches queued, inspect feature detection and the fallback constructor. If it reaches queued but not rendered, inspect callback errors and the visible assertion. If it renders stale data, inspect the taskId comparison and cancellation order. If only the duration bucket moves, repeat the same row before deciding that a priority change caused a regression.

A browser update can change implementation without changing the API name. Keep the source links and compatibility note close to the fixture, and re-check the MDN reference and WICG proposal before changing support language. Chrome's explanation is useful for the cooperative model, but an announcement is not evidence that an embedded view or another browser family ships identical behavior.

The practical handoff should contain the fixture revision, queue record, first changed field, and next safe action. A support engineer can then repeat one authorized page with taskId: results-17 rather than asking for all page contents. If the result is still ambiguous, report the missing state and stop. More logs cannot repair a fixture that never defined who owns the result.

Keep that handoff next to the release change. It lets a reviewer decide whether to ship, roll back, or schedule a narrower check without guessing from a priority name.

This boundary keeps a browser scheduling experiment reviewable: application correctness comes first, and queue hints remain one bounded input to the decision.

Keep cancellation visible in the same handoff so a reviewer can distinguish an intentional stop from a browser failure.

Keep queue telemetry minimal. A useful record is fixtureId, taskId, priority, implementation, outcome, and a bounded duration bucket. Remove temporary identifiers after the review window, and do not export callback arguments or page contents. Priority experiments should improve an owned interaction without becoming a reason to collect unrelated browser data.

When a task is retried, give the new attempt a new taskId and mark the old one superseded. On navigation or component teardown, abort owned controllers and release references. A task that completes after its owner is gone must not update a new route. This lifecycle check catches more production defects than a screenshot of a fast path.

The closest operational guides cover different boundaries: browser performance measurement explains how to measure an owned interval, while scaling browser contexts explains leases and capacity outside the page event loop. This article focuses on the browser task queue and the limits of treating priority as evidence.

Public sources

Practical conclusion

Choose the lowest priority that preserves the current user-visible contract, attach an owner-specific AbortSignal, and keep a bounded fallback for browsers without scheduler.postTask. Record queued, rendered, cancelled, failed, and superseded outcomes instead of treating priority as a latency promise. Re-run the same fixture after a browser update, and keep the result scoped to that fixture and environment.

#scheduler.postTask#Task Scheduling#Browser Performance#Web Platform

This article is part of the guide: Web Platform Surfaces: A Consistency Review Map

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.