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.
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
- Without yielding: one 90 ms task. A click at 10 ms must wait until it ends.
- With yielding: the work runs for 30 ms, then awaits a fresh task.
- The click was queued first, so it runs now — about 20 ms after the user clicked, not 80.
- Work resumes with the next chunk, then yields again.
- 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?
Yielding helps only if the chunks are short. A 100 ms chunk is still a long task; a key press that arrives during it waits for it to finish. Aim for chunks well under 50 ms — many teams use a time budget of 5–10 ms. The yield method matters less than the chunk size.
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.”
Follow-up Why does React's scheduler use MessageChannel instead of setTimeout(fn, 0)?
Follow-up Can you rely on scheduler.yield in every browser?
Follow-up What about navigator.scheduling.isInputPending?
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.
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.