INP: Scheduler API and Task Splitting
When to Use
Apply when interactions feel sluggish (INP >200ms), a processing loop blocks user input, or DevTools shows long tasks (red bar at top of main thread). The browser can only process user input between tasks — a task >50ms is a "long task."
The 50ms Rule: - Under 50ms → safe to run synchronously - 50–250ms → split the work and yield periodically - Over 250ms of computation → offload to a Web Worker
Decision: Yield Strategy
| Method | Queue position | Browser support | Use when |
|---|---|---|---|
scheduler.yield() |
Front of task queue — resumes before other pending tasks | Chrome 129+, Edge 129+, Firefox 142+; no Safari | Mid-task yield for INP-critical paths |
setTimeout(fn, 0) |
Back of task queue | All browsers | Cross-browser fallback |
requestIdleCallback |
Only when browser is idle | Chrome, Firefox; no Safari | Background work that can wait indefinitely |
requestAnimationFrame |
Before next paint | All browsers | Visual updates only |
LIMITED AVAILABILITY:
scheduler.yield()andscheduler.postTask()— Chrome 129+, Edge 129+, Firefox 142+; not supported in Safari as of June 2026. Always feature-detect and providesetTimeoutfallback.
Decision: scheduler.postTask() Priorities
| Priority | Use for |
|---|---|
user-blocking |
Input handling, critical rendering updates |
user-visible |
Non-blocking UI updates visible to the user (default) |
background |
Analytics, prefetching, telemetry |
Pattern
// Feature-detect once; reuse everywhere
async function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
}
// Process a large array without blocking user input
async function processLargeArray(items) {
let deadline = performance.now() + 50; // 50ms budget
for (const item of items) {
processItem(item);
if (performance.now() >= deadline) {
await yieldToMain(); // surrender main thread
deadline = performance.now() + 50;
}
}
}
// Defer work until scroll rests — scrollend is Baseline since 2025-12-12
scroller.addEventListener('scroll', () => {
updateProgressIndicator(); // keep cheap
}, { passive: true });
scroller.addEventListener('scrollend', () => {
const section = findMostVisibleSection(scroller);
fetchAdditionalData(section); // safe to do layout reads + fetches here
});
Common Mistakes
- Wrong: Relying solely on
setTimeout(fn, 0)as a yield → Right: Usescheduler.yield()for INP-critical paths;setTimeoutplaces continuations at the back of the task queue - Wrong: Heavy polling with
setInterval→ Right: Restructure as event-driven or userequestIdleCallback - Wrong: Yielding inside synchronous handlers → Right: Mark the handler
asynctoawaityieldToMain() - Wrong: DOM updates or content fetches on every
scrollevent → Right: Usescrollendor throttle with rAF
See Also
- INP: Field Measurement — measure which tasks are causing poor INP before optimizing
- Reference: web.dev: Optimize Long Tasks