Breaking Up Long Tasks: A Developer-Friendly Guide to Faster INP
Long tasks are the single biggest contributor to poor INP. When JavaScript runs for more than 50ms without yielding, the browser can’t process user input until that task completes. Breaking long tasks into smaller chunks—yielding control back to the browser between chunks—is the most effective technique for improving interaction responsiveness.
Why 50ms is the threshold
The browser’s main thread handles everything: JavaScript execution, style calculation, layout, paint, and input processing. When JavaScript runs, nothing else can happen on the main thread. If a user clicks while JavaScript is executing, the click waits in a queue.
50ms is the threshold because it aligns with human perception. Interactions completing within 100ms feel instant. If a long task blocks for 50ms and the event handler takes 50ms, total interaction time hits 100ms—at the edge of perception. Tasks longer than 50ms risk pushing total interaction time above the perceptible threshold.
The 200ms INP threshold allows for some queuing and processing time, but long tasks eat into that budget quickly. A 300ms long task leaves zero room for event handler processing and rendering. Breaking that task into 40ms chunks means the browser can process input between chunks.
The simplest yield: setTimeout(0)
The most basic yielding technique wraps remaining work in setTimeout with a zero delay. This doesn’t add a 0ms delay—it pushes the remaining work to the next task in the event loop, allowing the browser to process any pending input in between.
This pattern works well for loops processing arrays of items. Instead of processing 1,000 items in one task, process 50 items, yield with setTimeout(0), then process the next 50. Each chunk completes quickly, and the browser can handle user input between chunks.
The overhead is minimal—each yield adds roughly 4ms of scheduling overhead. For most use cases, this overhead is insignificant compared to the responsiveness improvement. The tradeoff is slightly longer total processing time in exchange for dramatically better interaction responsiveness.
scheduler.yield(): the modern approach
The scheduler.yield() API (available in Chrome 129+) provides a more sophisticated yield mechanism. Unlike setTimeout, yielded tasks resume at the front of the task queue rather than the back. This means your work continues promptly after yielding while still allowing input processing.
This matters because setTimeout(0) puts your continuation at the back of the task queue. If other tasks are queued (analytics, third-party scripts), your continuation waits behind them. scheduler.yield() prioritises your continuation while still allowing input processing. Your work completes faster overall.
Use scheduler.yield() with a fallback for browsers that don’t support it yet. Check 'scheduler' in globalThis && 'yield' in scheduler before using it. Fall back to setTimeout(0) when unavailable. This progressive enhancement provides the best experience where supported.
requestAnimationFrame for visual updates
When your long task involves visual updates (DOM manipulation, canvas drawing, animation calculations), requestAnimationFrame (rAF) is the appropriate yield mechanism. It schedules your continuation before the next paint, ensuring visual updates happen at the right time.
Use rAF when the work produces visual output. DOM updates, style changes, and animation frames should synchronise with the browser’s paint cycle. Using setTimeout for visual work can cause updates at odd times, potentially missing paint frames or causing visual jank.
Don’t use rAF for non-visual work. rAF fires once per frame (typically every 16ms at 60fps). Using it for data processing wastes frames where the browser could be idle. Use setTimeout(0) or scheduler.yield() for non-visual computation; use rAF only for visual updates.
Practical patterns for common scenarios
Processing large arrays: Split the array into chunks. Process one chunk per task. Use a generator function or index tracking to resume where you left off.
Complex DOM updates: Batch DOM reads first (measuring elements), then batch DOM writes (modifying elements) with a yield between reads and writes. This prevents layout thrashing and allows input processing between measurement and modification.
Initialisation sequences: If your page initialisation involves multiple independent steps (initialise analytics, set up event listeners, load secondary content), run each step in its own task rather than sequentially in one long task.
Event handler work: If a click handler needs to do expensive work (filter a large list, recalculate a layout, fetch and process data), perform the minimum work needed for visual feedback first (show a spinner, highlight the clicked element), then yield and do the expensive work in subsequent tasks.
Measuring improvement
Use the Performance panel in DevTools to record interactions before and after splitting tasks. The main thread timeline should show shorter individual task blocks with gaps between them—gaps where the browser can process input.
Compare the “Total Blocking Time” metric before and after. This aggregate measure captures how much time long tasks occupy. Successful task splitting reduces TBT significantly, which correlates with INP improvement.
Test on throttled CPU (4-6x slowdown) to simulate real-world devices. Task splitting that seems unnecessary on a fast machine becomes critical on slower devices where each millisecond of blocking is amplified.
Monitor field INP data after deploying changes. Lab testing confirms the technical fix, but field data from real users across real devices confirms the real-world impact. The 28-day rolling window in CrUX means patience is needed, but trends should be visible within 2-3 weeks.
Common mistakes when splitting tasks
Yielding too frequently adds overhead without benefit. If your tasks are already under 50ms, splitting them further adds scheduling overhead without improving responsiveness. Profile first to identify which tasks actually exceed the threshold.
Losing context across yields requires careful state management. When you yield between chunks, local variables may be in an intermediate state. Ensure each chunk handles partial state correctly, and consider whether interruption could cause visible inconsistencies.
Not prioritising visual feedback undermines the purpose. If you split a task but still block for 200ms before showing any visual response, the user perceives the same delay. Always perform the minimum visual update first, then yield for remaining work.
Forgetting to handle cancellation creates bugs. If the user triggers another interaction while chunked work is in progress, the old work should stop. Track in-flight work and cancel previous operations when new interactions supersede them.
When not to split tasks
Critical rendering path work that must complete before the first paint should not yield. If yielding would cause a visible delay in initial rendering, the cure is worse than the disease. Optimise this work through other means (reduce amount, move to worker, eliminate unnecessary steps).
Security-sensitive operations that must be atomic should not yield mid-operation. Authentication, encryption, and transaction-like updates should complete in one task to prevent inconsistent states visible to the user.
Very short tasks (under 30ms) don’t need splitting. The overhead of yielding exceeds the benefit. Focus on tasks that actually exceed the 50ms threshold.
The discipline of breaking up long tasks is the most impactful INP optimisation technique available. It requires understanding your code’s execution patterns and thoughtfully inserting yield points. The reward is dramatically improved responsiveness, especially on the mid-range and low-end devices your real users carry. For sites where identifying and splitting long tasks requires performance expertise, our performance service includes JavaScript profiling and targeted refactoring recommendations.