# Event Loop & scheduler.yield() > Break long tasks to keep the main thread responsive and hit INP. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/event-loop-scheduler-yield Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## 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)`. ## Diagram ```mermaid sequenceDiagram participant Q as Task queue participant M as Main thread participant R as Renderer Q->>M: Pick next task (macrotask) M->>M: Run task JS M->>M: Drain microtask queue M->>R: Style + layout + paint (if frame due) R->>Q: Next task ``` Each event-loop iteration: one macrotask, then all microtasks, then a render frame if the browser decides one is due. ## How 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. **Batch-process with scheduler.yield()** ```ts async function processItems(items: string[]): Promise { // Feature-detect; fall back to setTimeout for Safari. const yieldFn: () => Promise = typeof scheduler !== "undefined" && scheduler.yield ? () => scheduler.yield() : () => new Promise((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) | ## Watch out: 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. ## 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. ## Related topics - [Core Web Vitals](https://fearchitect.com/topics/core-web-vitals.md): Google's three user-experience metrics: LCP, INP, and CLS. - [Render Performance](https://fearchitect.com/topics/render-performance-patterns.md): Skip renders, defer slow work, and virtualize long lists. - [Web Workers & Off-Main-Thread](https://fearchitect.com/topics/web-workers-off-main-thread.md): Offload CPU-heavy work to a background thread, keeping the main thread free. - [Paint & Composite Optimization](https://fearchitect.com/topics/paint-composite-optimization.md): Animate only transform and opacity to skip paint entirely. - [The Rendering Pipeline](https://fearchitect.com/topics/rendering-pipeline.md): Style → Layout → Paint → Composite: what triggers each stage. - [WebAssembly on the Frontend](https://fearchitect.com/topics/webassembly.md): Portable bytecode that runs near-native in a sandboxed browser VM. ## Further reading - [MDN — Scheduler: yield()](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield) - [MDN — Scheduler: postTask()](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask) - [web.dev — Optimize long tasks](https://web.dev/articles/optimize-long-tasks) - [web.dev — Interaction to Next Paint (INP)](https://web.dev/articles/inp)