Skip to content
Core Web Vitals & measurement

Break Up Long Tasks: Lower INP with scheduler.yield

Break long JavaScript tasks over 50 ms into chunks with scheduler.yield(), free the main thread between the chunks and keep the INP steadily under 200 ms.

12 min read INPJavaScriptMain Threadscheduler.yieldCore Web Vitals

A click that reacts only after what feels like an eternity is one of the most common frustrations on the web – and almost always the same cause is behind it: the browser's main thread is busy with a long JavaScript task and cannot get around to handling the input. This is exactly what the Core Web Vital INP, Interaction to Next Paint, measures. As long as a single task runs, the browser can neither process the click nor paint the next frame; the page feels frozen. The fix sounds simple, and at its core it is: break long tasks into small chunks and briefly free the main thread between the chunks so that waiting clicks are handled at once. For a long time this yielding was awkward and came with side effects. With scheduler.yield(), available since Chrome 129 (Chrome for Developers), there is now a function made for exactly this – and it is supported in more and more browsers in 2026. This article shows when a task becomes a problem, how scheduler.yield() frees the main thread, why it works better than the old setTimeout trick, and how to prove the effect in your field data. In our Core Web Vitals optimization, breaking up long tasks is one of the most effective levers for a good INP value.

Key takeaways

  • The browser does almost everything on a single main thread. Any task of 50 milliseconds (web.dev) or more counts as a long task; while it runs, input has to wait, and on a 60-hertz display only about 16 milliseconds (web.dev) remain per frame.
  • Good means an INP under 200 milliseconds (web.dev), evaluated at the 75th percentile (web.dev). Since March 2024 (web.dev) INP has been a Core Web Vital and, unlike the earlier First Input Delay, measures the full time to the visible result.
  • scheduler.yield() frees the main thread and then resumes your own work with priority, because the continuation goes to the front of the queue. The function is available in Chromium-based browsers from Chrome 129 (Chrome for Developers).
  • The old setTimeout route puts the continuation at the end of the queue and, after 5 nested timers, is subject to a minimum delay of 4 milliseconds (MDN). It works as a fallback but no longer as the default route.
  • Do not yield after every iteration: a time budget of around 50 milliseconds (web.dev) works better, combined with navigator.scheduling.isInputPending(). Verify with Total Blocking Time in the lab and INP in the field data.

Why Long Tasks Block the Response

The browser does almost everything on a single main thread: it evaluates JavaScript, computes layout, paints frames and processes input such as clicks, keystrokes and scrolling. Because this thread can only do one thing at a time, the rule is: while it is busy with a long computation, every input has to wait. The Chrome team counts any task of 50 milliseconds (web.dev) or more as a long task, because beyond that duration the risk grows that an interaction is not answered in time. The RAIL model recommends responding to input within 50 milliseconds (web.dev) so that the response feels immediate. For comparison: on a 60-hertz display there are only about 16 milliseconds (web.dev) per frame – so a task of several hundred milliseconds swallows dozens of frames in a row and makes the page stutter.

INP condenses this experience into a single number. Across the entire page visit it observes all clicks, keystrokes and taps and reports, in effect, the longest observed delay – evaluated at the 75th percentile (web.dev) of interactions so that individual outliers do not distort the value. A good INP is under 200 milliseconds (web.dev); between 200 and 500 milliseconds (web.dev) there is room for improvement, and above 500 milliseconds (web.dev) the value is poor. Since March 2024 (web.dev), INP has been one of the three Core Web Vitals, replacing the old First Input Delay whose threshold was still 100 milliseconds (web.dev). The difference is decisive: FID only measured the delay until processing began, whereas INP measures the whole time until the visible result – and thus exposes long tasks without mercy.

What Exactly Is a Long Task?

A long task is any contiguous task on the main thread that runs longer than 50 milliseconds (web.dev). During that time the thread is blocked: clicks, keystrokes and animations have to wait. The lab metric Total Blocking Time sums, for each long task, the time above the 50-millisecond mark (web.dev) and is therefore a good indicator of how responsive a page will later be in the field. Lowering Total Blocking Time typically improves INP as well.

What scheduler.yield Does Differently from setTimeout

The idea of freeing the main thread in between is not new. Classically, people used setTimeout with zero milliseconds delay: they interrupted the work, re-queued the rest via a timer and thus gave the browser a chance to process input in between. The trick works but has two drawbacks. First, the resumed part lands at the back of the task queue – behind everything else the browser has scheduled, including third-party scripts. Second, the HTML specification enforces a minimum delay of 4 milliseconds (MDN) after 5 nested timers, which adds up across many chunks. The result: the work does yield, but afterwards it may not resume for a long time.

scheduler.yield() solves exactly this problem. The call also frees the main thread so the browser can handle waiting clicks and pending rendering – but afterwards it resumes your own task with priority. The continuation is placed at the front of the queue rather than the back, so the work already begun is not overtaken by newly arrived tasks. You get the best of both worlds: the browser stays responsive, and the computation still finishes quickly. scheduler.yield() returns a promise and is used with await – the code reads almost like an ordinary loop. The function is available in Chromium-based browsers since Chrome 129 (Chrome for Developers), which shipped in September 2024 (Chrome for Developers), and support is broadening through 2026.

yield-to-main.js
// Hand off to the main thread, with a fallback for older browsers
async function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }
  // Fallback: put the continuation at the back of the task queue
  return new Promise((resolve) => setTimeout(resolve, 0));
}

So that the technique also works in browsers that do not yet know scheduler.yield(), you wrap the call in a small helper function with feature detection. If the function exists, it is used; otherwise the code falls back cleanly to the proven setTimeout path. This way modern browsers benefit from the prioritized continuation, while older ones at least get the basic yield – without the application breaking on a missing function.

Break a Long Loop into Chunks

The most common trigger for long tasks is a loop that iterates over many items and performs a not-quite-cheap operation for each – for example preparing a large product list, processing a bulky JSON response, or creating many DOM nodes. If the code runs the whole loop in one go, a single long task results. The fix is to hand off briefly to the main thread after each step – or after a group of steps. In the simplest case an await on the helper function in the middle of the loop is enough.

process-entries.js
async function processEntries(entries) {
  for (const entry of entries) {
    processEntry(entry);   // expensive per-item work
    await yieldToMain();   // free the main thread
  }
}

Do Not Yield After Every Iteration

As tempting as it is to hand off after every single loop pass – it can noticeably lengthen the total duration. Each yield costs a small overhead, and if the per-item work only takes a millisecond anyway, that overhead quickly outweighs the benefit. A time budget is smarter: the code works in one stretch until it has used about 50 milliseconds (web.dev), and only then yields. This keeps every chunk under the long-task limit without interrupting more often than necessary.

Yield Only When Input Is Pending

You can steer even more finely by yielding only when input is actually waiting. The browser function navigator.scheduling.isInputPending() reveals whether a click or keystroke is in the queue. Combined with a time budget, this yields a pattern that frees the main thread only when it pays off: either because the budget is used up or because a user is interacting right now. In quiet phases the work runs through without interruption; in busy ones it responds immediately. The optional-chaining operator keeps the code robust if a browser does not offer the function.

in-chunks.js
async function inChunks(entries) {
  let lastYield = performance.now();
  for (const entry of entries) {
    processEntry(entry);
    // Yield only when 50 ms are used up or input is pending
    if (performance.now() - lastYield > 50 ||
        navigator.scheduling?.isInputPending?.()) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

Preparing Large Data Sets

Filtering, sorting or transforming long lists in the browser is a classic among long tasks. A time budget with yielding keeps the interface usable while the computation runs in the background.

Building Lots of DOM at Once

When a large table or list is created in a single pass, the insertion blocks the thread. Split into chunks, the content appears staggered, but the page stays responsive.

Initializing Much on Load

Init scripts, hydration and deferred modules often do a lot at once. Spreading the work across several chunks prevents a long block right after the first render.

Alternatives and the Fallback

scheduler.yield() is not the only tool for task scheduling. The related function scheduler.postTask() schedules whole tasks with one of 3 priority levels (MDN) – user-blocking, user-visible and background – and is suited to stagger work by importance from the outset. requestIdleCallback, in turn, pushes non-critical work into the browser's idle time. For simply splitting a long, contiguous task, however, scheduler.yield() is the most direct choice because it resumes the work already begun with priority. In browsers without support, the setTimeout fallback remains – it only returns to the back of the queue, but at least it prevents the long block.

MethodFrees main threadContinuationBest use
setTimeout(fn, 0)included Back of the queue, 4 ms clampFallback for older browsers
scheduler.postTask()Schedules whole tasksBy priority levelStagger work by importance
requestIdleCallback()During idle timeWhen time is leftNon-critical background work
scheduler.yield()included Prioritized, front of the queueSplit a long task into chunks

The Core in One Sentence

It is not the fastest computation that makes a page responsive, but the right interruption: break a long task into chunks under 50 milliseconds and free the main thread in between with scheduler.yield() – so the click comes before the finish.

How to Measure the Effect

Whether the splitting works can be checked with freely available tools. In the lab, the Lighthouse report shows Total Blocking Time and marks long tasks; if the number of blocks over 50 milliseconds (web.dev) drops, the split succeeded. The performance panel of the Chrome developer tools shows each task as a bar – long tasks are flagged and can be compared directly before and after the change. For root-cause analysis in real sessions, the Long Animation Frames API precisely attributes which script held up a frame; how to find INP bottlenecks in the frame with it is covered separately. The open-source web-vitals library additionally reports INP from the field data of real users.

  1. Identify long tasks in the Lighthouse report or the performance panel
  2. Determine the triggering function – usually a loop or an init block
  3. Create a yieldToMain helper with feature detection and a fallback
  4. Split the work into chunks with a time budget of about 50 milliseconds
  5. Yield to the main thread only when the budget is used up or input is pending
  6. Measure before and after – Total Blocking Time in the lab, INP in the field

It is important to measure in the lab with realistic throttling: a fast office machine hides exactly the long tasks that become a problem on an average smartphone. That is why every optimization includes a look into the field – only there does it show what real users experience on their devices. And because new long tasks easily creep back in, for instance through an added script or a grown data set, it pays to monitor the values continuously and stop regressions early, as we describe in the performance budget in CI.

From Task Splitting to a Stable INP

Splitting long tasks is rarely the only measure, but often the most effective one for responsiveness. Frequently the longest tasks do not even come from your own code but from embedded third-party scripts – ad, analytics or chat modules that do a lot at once on load. How to trim third-party scripts is therefore the natural second step. A deliberate JavaScript performance budget helps in addition, so that too much code does not run at once in the first place. And anyone who wants to understand INP in the context of the other two metrics will find the classification in the article on Core Web Vitals 2026 as well as in the overview on optimizing INP deliberately.

Responsiveness does not come from computing everything faster, but from letting the browser breathe at the right moment. A long task, cleverly split into chunks, immediately feels smoother to the user – and INP follows that experience.

Project experience from 50+ performance projects

This is exactly how we proceed in a structured performance audit: track down long tasks, name the triggering functions and break them into responsive chunks with scheduler.yield() and a clean fallback – then we verify in your field data whether INP stays in the green for good. Which services this includes is shown in our overview of performance services; the concrete frontend levers we bundle in frontend optimization.

This article is based on data from: Chrome for Developers (Use scheduler.yield() to break up long tasks) and web.dev (Optimize INP, top-cwv, Total Blocking Time, RAIL) as well as MDN Web Docs (Scheduler.yield, Scheduler.postTask, setTimeout). All values cited were verified at the time of publication.

Related Articles

Fundamentals & strategy

JavaScript and Performance Budgets: Less Code, Faster Sites

Minimize JavaScript impact on INP and loading time: performance budgets, code splitting, tree shaking and third-party management for Core Web Vitals.

14 min read
Front-end optimisation

Frontend Memory Leaks: When Long Sessions Slow Down

The interface starts fast and turns sluggish after twenty minutes: how front-end memory leaks arise, how to measure them over time and how to get rid of them.

12 min read
Core Web Vitals & measurement

Web Workers: Offload the Main Thread to Improve INP

Offloading compute-heavy JavaScript to a web worker keeps the main thread free, lowers Total Blocking Time and brings INP under the 200 ms threshold.

12 min read