Skip to content

Break Up Long Tasks

Heavy computations or long loops can block the main thread, delaying event handlers and causing poor Interaction to Next Paint (INP) responsiveness. To prevent this, yield control back to the browser periodically with scheduler.yield(), which pauses an already-running task so the browser can handle pending user input and rendering before continuing.

scheduler.yield() only splits a single running task—it does not order separate work streams ('user-blocking', 'user-visible', 'background'), defer non-urgent background work before it starts, or abort and reprioritize queued tasks via TaskController. DO NOT replace scheduler.postTask() or TaskController with scheduler.yield() alone when scheduling work at different priorities; see schedule-tasks-by-priority (guide not yet published on this site). When a background task scheduled with scheduler.postTask() itself exceeds 50ms, combine both APIs by calling await scheduler.yield() inside the postTask() callback—continuations automatically inherit the enclosing task’s priority and TaskSignal.

Use scheduler.yield() inside async functions to break up work.

async function processLargeArray(items) {
// DO: Set a time-based deadline 50 milliseconds into the future. 50
// milliseconds is the boundary for when a task becomes a long task.
let deadline = performance.now() + 50; // 50ms budget
for (const item of items) {
// Process the item
processItem(item);
// MANDATORY: Yield to the main thread periodically to keep the UI
// responsive. This can be done by checking if the deadline set earlier
// has been exceeded. When it has been, yield, then reset the deadline
// another 50 milliseconds into the future.
if (performance.now() >= deadline) {
await scheduler.yield();
deadline = performance.now() + 50;
}
}
}

Baseline: scheduler

If your Baseline target does not support the Scheduler API, implement a fallback using setTimeout so long loops still yield to the main thread without throwing. (If combining scheduler.yield() with scheduler.postTask(), feature-detect postTask separately as well.)

async function processLargeArrayWithFallback(items) {
// DO: Set a time-based deadline 50 milliseconds into the future.
let deadline = performance.now() + 50;
for (const item of items) {
processItem(item);
// MANDATORY: Yield to the main thread periodically to keep the UI responsive.
if (performance.now() >= deadline) {
// DO: Feature detect scheduler.yield
if ('scheduler' in window && 'yield' in window.scheduler) {
await scheduler.yield();
} else {
// DO: Fallback to setTimeout for older browsers
await new Promise(resolve => setTimeout(resolve, 0));
}
deadline = performance.now() + 50;
}
}
}

Compatibility notes: Badges like Baseline: … mark web features referenced by this guide. Baseline status describes availability across the core browser set — it does not mean every browser or older version supports the feature. Feature data as published upstream, synced 2026-11-12.

Open the demo standalone · Demo source on GitHub

Counter-example (negative demo) — what this guide tells you to avoid.


Source: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.