Summary
The browser event loop processes one task at a time on the main thread. Any task exceeding 50 ms blocks input handling and worsens INP. `scheduler.yield()` lets long work pause mid-loop so the browser can handle a pending click before resuming — with a prioritized continuation that avoids the starvation risk of `setTimeout(0)`.
Jump to the interview angleHow a long task hurts INP
- 1
User clicks a button
The browser queues a click event as a macrotask. It cannot dispatch it until the currently running task finishes.
- 2
A long task blocks the thread
Your event handler or a third-party script runs for 300 ms. No other tasks execute; the click sits in the queue.
- 3
Input delay accumulates
The browser finally processes the click only after the long task completes. That 300 ms gap counts directly toward INP.
- 4
Yield and resume
Inserting
await scheduler.yield()mid-task ends the current task, lets the browser dispatch the click, then resumes your work in a new prioritized task.
Breaking a long loop with scheduler.yield()
Process items in batches and yield every 50 ms. The continuation runs at user-visible priority so it resumes before background tasks but after any pending input events.
async function processItems(items: string[]): Promise<void> {
// Feature-detect; fall back to setTimeout for Safari.
const yieldFn: () => Promise<void> =
typeof scheduler !== "undefined" && scheduler.yield
? () => scheduler.yield()
: () => new Promise<void>((resolve) => setTimeout(resolve, 0));
let deadline = performance.now() + 50;
for (const item of items) {
doWork(item);
if (performance.now() >= deadline) {
await yieldFn(); // pause; browser handles input here
deadline = performance.now() + 50;
}
}
}Yield every 50 ms so no single task exceeds the long-task threshold. Safari falls back to setTimeout(0) — no priority guarantee, but still yields.
Yield mechanisms compared
| API | Priority | Support | Continuation guarantee | |
|---|---|---|---|---|
| scheduler.yield() | user-visible (inherits postTask priority) | Chromium + Firefox; not Safari | Resumes before background tasks | |
| setTimeout(0) | No priority — joins back of task queue | All browsers | No; any queued task may run first | |
| scheduler.postTask() | user-blocking / user-visible / background | Chromium + Firefox; not Safari | Yes, per declared priority | |
| MessageChannel | Roughly macro-task level, no priority | All browsers | No; similar to setTimeout(0) |
Avoid isInputPending()
navigator.scheduling.isInputPending() was an early yield-heuristic. web.dev now recommends against it: it can return false during real input and does not account for animation frames. Use scheduler.yield() with a time-based deadline instead.
Interview angle
Interviewers probe whether you know why long tasks hurt INP and how to break them without losing work. Describe the task → microtask → render cycle, then explain why scheduler.yield() beats setTimeout(0) for continuations.
Soundbite: "scheduler.yield() pauses a task and re-queues its continuation at user-visible priority, so the browser handles input before resuming your work."
Key terms
- macrotask
- A single unit of work the event loop picks from the task queue: a script, event callback, or timer.
- microtask
- Work queued via Promise resolution or `queueMicrotask`; drains entirely after each task before rendering.
- long task
- Any main-thread task exceeding 50 ms; blocks input handling and increases INP.
- INP
- Interaction to Next Paint — 98th-percentile input-to-paint delay; good ≤200 ms.
- scheduler.yield()
- Prioritized Task Scheduling API method that pauses a task and re-queues its continuation at user-visible priority.