Engrain by ByteRush

JS.01.07 Senior· 11 minCore — asked often

Yielding to the main thread: keep pages responsive during heavy work

Long tasks make clicks wait and hurt INP. Split work into chunks, yield with scheduler.yield or a timer, and know what React's scheduler does.

Comes up in: Web fundamentals, Frontend system design

Learn#

Neha’s search page filters 50,000 products on every keystroke. The filter takes 300 ms. Typing feels broken: letters appear in bursts, and the page’s INP (Interaction to Next Paint — how long the page takes to respond to input) is far above the 200 ms “good” limit.

Any task over 50 ms is a long task. While it runs, clicks and keys wait in the queue.

The fix is not less work. It is shorter tasks. Do a little, then yield: give the event loop a turn, so waiting input can run, then continue in a new task.

const log = [];
setTimeout(() => log.push("click handler"), 0); // stands in for user input
const busy = (ms) => { const t = performance.now(); while (performance.now() - t < ms) {} };

async function work() {
  for (let chunk = 1; chunk <= 3; chunk++) {
    busy(30);
    log.push("chunk " + chunk);
    await new Promise((r) => setTimeout(r, 0)); // yield
  }
  console.log(log.join(" -> "));
}
work();

Output (verified)

chunk 1 -> click handler -> chunk 2 -> chunk 3

Without the yield, the click handler would run last, after all 90 ms.

Browsers now offer a direct way: await scheduler.yield() continues before other ordinary tasks, so you do not lose your place in line. scheduler.postTask(fn, { priority }) adds task priorities: user-blocking, user-visible or background.

Yielding lets the click throughStep 1 of 5

Call stack

  • work (90 ms)

Task queue

  • click handler

Console

    empty

    Without yielding: one 90 ms task. A click at 10 ms must wait until it ends.

    Read the steps as text
    1. Without yielding: one 90 ms task. A click at 10 ms must wait until it ends.
    2. With yielding: the work runs for 30 ms, then awaits a fresh task.
    3. The click was queued first, so it runs now — about 20 ms after the user clicked, not 80.
    4. Work resumes with the next chunk, then yields again.
    5. Same total work, but no task is long, so input never waits long.

    Predict#

    Predict first

    You split a 600 ms job into 6 chunks of 100 ms, yielding between them. Is the page's input delay now good?

    Interview angle#

    Say it like this — about 30 seconds

    “Input waits for the current task, so long tasks over 50 ms hurt INP. I split heavy work into small chunks with a time budget, around 5 to 10 ms, and yield between them. Where supported I use scheduler.yield(), which keeps my place in the queue; otherwise a timeout or a MessageChannel message. For real CPU work I move it to a Web Worker. I check the result with INP field data and the long tasks in a performance trace.”

    Stays on your device. Nothing is uploaded.
    Follow-up Why does React's scheduler use MessageChannel instead of setTimeout(fn, 0)?
    A posted message is a new task without timer clamping, so React can yield and resume with almost no delay.
    Follow-up Can you rely on scheduler.yield in every browser?
    No. Chrome and Firefox support it; Safari does not. Feature-detect it and fall back to a timeout or MessageChannel.
    Follow-up What about navigator.scheduling.isInputPending?
    It is Chrome-only and experimental. Chrome’s guidance now prefers yielding on a time budget with scheduler.yield.

    Practice#

    Timed, like the real round

    Make processInChunks yield after about budgetMs of work. The tests use slow items and count how long you hold the thread.

    Done when

    • Every item is processed once, in order.
    • No stretch of work between yields is much longer than budgetMs.
    • With no yieldFn, the default yield really hands control back; short work does not yield at all.
    Your solution
    Ctrl/⌘ + Enter

    Remember#

    Try to answer each card before you open it. Then add them to your reviews.

    What is a long task?

    A task that keeps the main thread busy for more than 50 ms.

    How does yielding improve responsiveness?

    Work runs in short tasks, so input waiting in the queue gets a turn between them.

    What does scheduler.yield() do that setTimeout(fn, 0) does not?

    It resumes your work ahead of other ordinary tasks, keeping your place.

    What time budget per chunk keeps input responsive?

    Well under 50 ms — often 5–10 ms.

    Go deeper

    Four layers. Stop at the one your interview needs; each ends with a quick check.

    The core ideaEvery round

    Input can only run between tasks. Keep every task short by doing work in small chunks and yielding between them. Total time may grow a little; responsiveness improves a lot.

    Check yourself: Does yielding reduce the total work?

    No. It splits the same work into shorter tasks.

    How it works underneathSenior rounds

    Tasks over 50 ms are “long tasks” in the Long Tasks API Source: W3C: Long Tasks API. INP is good at 200 ms or less, measured at the 75th percentile of real visits. The Prioritized Task Scheduling API gives postTask with priorities and scheduler.yield(), whose continuation is queued ahead of other tasks of the same priority Source: WICG: Prioritized Task Scheduling.

    Check yourself: Which three priorities does scheduler.postTask accept?

    user-blocking, user-visible (the default) and background.

    Edge cases and trapsWhere people slip

    Support for scheduler.postTask: Chrome 94, Firefox 142, Safari no. For scheduler.yield: Chrome 129, Firefox 142, Safari no. Because its continuation goes ahead of ordinary tasks, a timer can wait until all your chunks finish; the browser still lets input through. Feature-detect: globalThis.scheduler?.yield. Fall back to a MessageChannel message (no 4 ms clamping) or setTimeout. isInputPending is Chrome-only and experimental; do not build on it.

    Check yourself: Which browser does not support scheduler.yield?

    Safari.

    In productionStaff-level answers

    React’s scheduler yields about every 5 ms using MessageChannel in browsers. For filtering or parsing big data, a Web Worker removes the work from the main thread entirely (WEB.06.01). Measure with field INP data and a performance trace, and optimize the slowest interactions first (QLT.01.03) Source: web.dev: Optimize long tasks.

    Check yourself: When is a Web Worker better than yielding?

    For heavy CPU work that does not need the DOM, like parsing or filtering big data.

    Finished the five steps? Mark it, and its cards join your reviews.

    Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.