Interaction to Next Paint (INP): The Definitive Optimization Guide (2025)
In March 2024, Google officially replaced First Input Delay (FID) with Interaction to Next Paint (INP) as a Core Web Vital. This wasn't just a minor metric swap; it was a fundamental shift in how we measure web responsiveness.
For years, developers optimized for the first click. But users don't just click once. They type, they toggle, they drag, and they expect the interface to respond instantly every single time. FID only measured the first impression. INP measures the entire relationship.
If you've noticed your "Total Blocking Time" (TBT) is high, or your Lighthouse score is green but your real users are complaining about "laggy" UI, this guide is for you. We're going deep into the mechanics of the browser's main thread to understand why interactions get delayed and how to fix them.
The Anatomy of an Interaction
To optimize INP, you must understand what happens when a user clicks a button. It's not instantaneous. The browser goes through three distinct phases:
- Input Delay: The time from the user's tap until the browser starts executing the event handler. This is often caused by other tasks blocking the main thread (e.g., a hydration process running exactly when the user clicks).
- Processing Time: The time it takes to run your event callbacks (React state updates, DOM manipulation, complex logic).
- Presentation Delay: The time from when your code finishes running until the browser actually paints the next frame to the screen.
INP = Input Delay + Processing Time + Presentation Delay
Most developers focus solely on step 2 (optimizing their code). However, a massive chunk of poor INP scores comes from steps 1 and 3.
Why console.time() Is Lying to You
You might wrap your click handler in a console.time() and see it runs in 20ms. "Great!" you think, "My code is fast." But your INP score reports 400ms. Why?
Because console.time() only measures the JavaScript execution. It doesn't measure the rendering cost.
If your 20ms React state update triggers a re-render of the entire component tree, forcing the browser to recalculate styles and layout for 5,000 DOM nodes, that layout thrashing happens after your JavaScript finishes but before the next paint. That is Presentation Delay, and it counts against your INP.
Strategy 1: Yielding to the Main Thread
The golden rule of responsiveness is: Don't hog the main thread.
If you have a long-running task (e.g., processing a large array of data), the browser is "frozen." It can't handle new inputs or paint the screen. The solution is to break up long tasks into smaller chunks and "yield" control back to the browser in between.
The Old Way: setTimeout
function processData(items) { if (items.length === 0) return; // Process one item doHeavyWork(items[0]); // Schedule the rest for later setTimeout(() => { processData(items.slice(1)); }, 0); }
This works, but setTimeout has a minimum delay (often 4ms or more) and doesn't prioritize tasks intelligently.
The Modern Way: scheduler.yield()
The new scheduler.yield() API (currently in origin trial or polyfilled) allows you to yield to the main thread and immediately resume execution without the penalty of setTimeout.
async function processData(items) { for (const item of items) { doHeavyWork(item); // Yield to the main thread to let the browser handle inputs/painting // This checks if there is pending user input! await scheduler.yield(); } }
This is a game-changer. It tells the browser: "I'm pausing for a microsecond. If there's a click waiting, handle it now. If not, I'll keep going."
Strategy 2: Optimizing Presentation Delay
If your JavaScript is fast but the paint is slow, you have a rendering problem.
- Reduce DOM Size: A massive DOM tree increases the cost of Style Recalculation and Layout. Virtualize long lists.
- CSS Containment: Use the
content-visibility: autoproperty to skip rendering off-screen content. - Avoid Layout Thrashing: Don't read layout properties (like
offsetHeight) immediately after writing them. This forces the browser to perform a synchronous layout calculation.
Strategy 3: Immediate Feedback (Optimistic UI)
Psychologically, a user feels an app is "slow" if they don't see a reaction. Technically, INP measures the time to the next paint.
If you have a heavy operation (like an API call), paint something immediately.
Bad Pattern:
button.addEventListener('click', async () => { const data = await fetchData(); // Waits for network... renderData(data); // ...then paints. });
INP includes the network wait time!
Good Pattern:
button.addEventListener('click', () => { showSpinner(); // Paints immediately! INP stops measuring here. fetchData().then(data => { renderData(data); hideSpinner(); }); });
By showing a spinner or an active state immediately, you "complete" the interaction from the browser's perspective. The subsequent network request and final render are separate tasks that don't hurt your INP score.
Debugging INP in the Wild
Don't guess. Measure.
- Chrome DevTools: Use the Performance panel. Look for the "Interactions" track. Red bars indicate slow interactions. Click on them to see exactly which part (Input Delay, Processing, or Presentation) is the bottleneck.
- Web Vitals Extension: Install the Web Vitals Chrome extension to see your INP score in real-time as you browse your site.
- Real User Monitoring (RUM): Lab data isn't enough. Use a RUM provider (or Google's CrUX dashboard) to see what your actual users are experiencing. They might have slower devices than your MacBook Pro.
Conclusion
INP is a strict metric, but it forces us to build better software. It pushes us away from "JavaScript-heavy, main-thread-blocking" architectures towards "asynchronous, yielding, and responsive" designs.
Start by profiling your slowest interactions. Is it the logic? The rendering? Or just a busy main thread? Once you identify the bottleneck, the tools—scheduler.yield(), DOM virtualization, and optimistic UI updates—are ready for you to use.
Make your site feel instant. Your users (and your SEO rankings) will thank you.
Explore Related Tools
Try these free developer tools from Pockit