WSS
BlogPerformanceArchitecturePublished

Interaction to Next Paint (INP): Diagnosing and Optimizing Main Thread Blocking

Understand how to optimize Interaction to Next Paint (INP) by yielding to the main thread, debugging long tasks, and architectural changes.

If you monitor Google PageSpeed Insights or the Chrome User Experience Report (CrUX), you have likely encountered Interaction to Next Paint (INP) warnings.

Google officially replaced First Input Delay (FID) with INP as a Core Web Vital metric. Where FID only measured the delay of the very first interaction, INP tracks the latency of all interactions across the entire lifespan of the page, reporting the worst (or near-worst) score.

Here is a comprehensive technical guide to understanding how the browser main thread schedules tasks, exactly why INP regressions occur, and the architectural patterns required to fix them.

The Three Phases of an Interaction

When a user interacts with a page (via click, tap, or keypress), the resulting visual update is blocked until the browser completes a sequence of operations. This sequence is broken down into three distinct phases:

  1. Input Delay: The time between the physical user action and the browser executing the associated event handlers. This delay is usually caused by the main thread being blocked by a completely unrelated, currently executing long task.
  2. Processing Time: The time taken to execute the JavaScript event handlers themselves.
  3. Presentation Delay: The time the browser takes to calculate style recalculations, layout shifts, paint, and composite the new frame.
Architecture Diagram

To achieve a “good” INP according to Core Web Vitals, pages should aim for an INP of 200 ms or less at the 75th percentile of page visits.

Why INP Fails: The Main Thread Bottleneck

Although modern browsers use multiple internal threads (such as compositor, raster, and networking threads), JavaScript execution and most DOM, style, and layout work occur on the main thread. When that thread is blocked by a long-running task, the browser cannot promptly respond to user input or render updates.

Tasks that execute for longer than 50ms are officially classified as Long Tasks.

The Browser Event Loop

JavaScript does not execute continuously. Instead, the browser processes work through an event loop that repeatedly:

  1. Executes one task from the task queue.
  2. Runs pending microtasks.
  3. Renders a frame if needed.
  4. Handles user input.
  5. Repeats.

When a single task occupies the main thread for hundreds of milliseconds, the browser cannot return to the event loop to process input or paint the next frame. APIs such as scheduler.yield() improve responsiveness by voluntarily returning control to the event loop before continuing additional work.

Diagnosing Long Tasks

You cannot optimize INP by guessing. You must profile the main thread.

Lab vs. Field Data: Many developers are confused when Lighthouse reports a good INP but CrUX reports a poor one. Lighthouse is a lab simulation run on a clean browser. CrUX is field data measured from actual users (who may have slow devices or many browser extensions). Always optimize for the field data.

To diagnose in the lab:

  1. Open Chrome DevTools > Performance panel or Performance Insights panel.
  2. Check the Web Vitals checkbox. (You can also use the Live Core Web Vitals overlay).
  3. Start a recording and interact with the page.
  4. Stop the recording and look for red-striped blocks in the Main track. These indicate Long Tasks.
  5. In the Interactions track, hover over the logged interaction to see the exact breakdown of Input Delay, Processing Time, and Presentation Delay.

Framework Implications

INP issues today are often caused by frontend frameworks rather than vanilla JavaScript. A short note on common culprits:

  • React: Unnecessary rerenders or expensive Context updates blocking the thread.
  • Vue: Expensive reactive cascades.
  • Angular: Heavy change detection cycles.
  • General: Excessively large component trees, regardless of framework.

Architectural Solutions for INP

To resolve INP issues, you must restructure how your application schedules work.

1. Yielding to the Main Thread

If you have a massive array of data to process, do not run it in a single continuous for loop. You must break the work into smaller chunks and explicitly yield control back to the browser so it can render pending interactions.

Historically, developers used setTimeout(() => {}, 0) to yield. Today, modern browsers support scheduler.yield().

async function processMassiveData(items) {
  for (let i = 0; i < items.length; i++) {
    // Process one chunk of work
    computeHeavyTask(items[i]);
    
    // Periodically yield back to the browser's main thread
    if (i % 50 === 0 && 'scheduler' in window) {
      await scheduler.yield();
    }
  }
}

The Prioritized Task Scheduling API also introduces scheduler.postTask(), allowing applications to schedule work with priorities such as user-blocking, user-visible, and background. While browser support is still evolving, it provides a more expressive scheduling model than setTimeout().

[!NOTE] Browser Support: Because the Prioritized Task Scheduling API is still being adopted across browsers, production applications should feature-detect these APIs ('scheduler' in window) and provide fallbacks where necessary.

2. Moving Work to Web Workers

If the processing logic is extremely heavy (e.g., sorting 10,000 products, complex client-side search, or image manipulation), do not run it on the main thread at all. Move it to a Web Worker.

Web Workers execute in a separate background thread. They cannot access the DOM, but they can perform intense CPU operations while the main thread remains instantly responsive to user clicks.

3. Avoiding Layout Thrashing

If your INP failure is driven primarily by Presentation Delay, the issue is likely DOM thrashing.

If your JavaScript repeatedly reads a layout property (like element.offsetHeight) and immediately writes to a layout property (like element.style.height) within a loop, you force the browser to synchronously recalculate the entire page layout multiple times per frame.

Solution: Read all layout properties first, then batch all DOM writes at the end. Use requestAnimationFrame() to perfectly sync visual updates with the browser’s rendering cycle.

Which Approach Should You Choose?

ScenarioPrimary BottleneckRecommendation
Heavy UI Rendering (e.g., React Context churn)Processing TimeComponent memoization, chunked rendering, yield to main thread.
Complex Math / Data SortingInput / Processing DelayMove logic entirely off the main thread into a Web Worker.
Third-Party Ads/AnalyticsInput DelayDefer third-party scripts until requestIdleCallback or user interaction.
Slow CSS Animations / ResizingPresentation DelayStop reading/writing DOM in loops. Use CSS transforms (translate, scale) instead of layout properties (width, top).

Scheduling API Comparison

APIBest Used For
setTimeout()Legacy deferral
requestAnimationFrame()Visual updates
requestIdleCallback()Non-critical background work
scheduler.yield()Breaking long tasks
scheduler.postTask()Priority scheduling
Web WorkersHeavy CPU computation

Architecture Summary

A healthy, INP-optimized frontend architecture relies on asynchronous task chunking and off-main-thread processing:

Architecture Diagram

Glossary

  • INP (Interaction to Next Paint): A metric tracking the latency of all interactions across the page lifecycle.
  • FID (First Input Delay): The deprecated predecessor to INP that only measured the input delay of the very first interaction.
  • Long Task: Any JavaScript execution that blocks the main thread for more than 50 milliseconds.
  • scheduler.yield(): A modern web API that allows developers to voluntarily pause a JavaScript function to let the browser process urgent user inputs.

Specification Status

TechnologyStatus
Interaction to Next Paint (INP)Core Web Vital
Web WorkersWeb Standard
requestAnimationFrameWeb Standard
scheduler.yield()Emerging API
scheduler.postTask()Emerging API

Revision History

2026-07

  • Initial documentation for Interaction to Next Paint.
  • Added guidance on scheduler.yield() and Web Worker offloading.

References

Related posts