INP optimization is critical for responsive apps. Learn how to identify main thread blocking, break up long tasks, and keep your UI snappy for users.

Last month, I spent three days chasing a sluggish "Add to Cart" button that felt like it was stuck in molasses. The metrics were screaming—our Interaction to Next Paint (INP) was hovering around 450ms, which is well into the "Poor" territory according to Core Web Vitals.
If you've ever felt that phantom delay after clicking a button, you know exactly what I’m talking about. It’s not just a technical stat; it’s a broken promise to the user. Here’s how I finally untangled our main thread and brought that number down.
INP measures the latency of all interactions—clicks, taps, and keyboard inputs—from the moment a user initiates an action until the next frame is painted on the screen. It’s not just about how fast your JavaScript runs; it’s about how much junk you've piled onto the browser’s main thread.
When the main thread is busy executing a massive loop or a bloated framework hook, it can't respond to user input. This is where INP optimization becomes a game of "clear the path." Every long task—anything blocking the main thread for more than 50ms—is an enemy.
We initially thought the delay was due to API latency, so we spent hours on Next.js App Router Data Fetching: Avoiding Performance Waterfalls. While that improved our initial load, the "Add to Cart" interaction remained sluggish. We were looking at the server when we should have been looking at the browser's event loop. It turns out, we were triggering a massive state update that forced a deep re-render of our entire navigation tree.

To fix the issue, I pulled up the Chrome DevTools Performance tab. I recorded the interaction and looked for the red triangles at the top of the timeline. Those triangles mark long tasks.
I found a 180ms block caused by a synchronous useEffect hook that was calculating cart totals on every single click. It was inefficient, and more importantly, it was entirely unnecessary to run it before the UI updated.
setTimeout or scheduler.yield() to break it into two 50ms chunks. This allows the browser to breathe and process input in between.requestIdleCallback.memo or useMemo is the difference between a 30ms response and a 200ms hang.The most effective tool I’ve found for INP optimization is manual yielding. It’s a simple concept: if you have a sequence of operations, yield control back to the browser periodically.
JAVASCRIPTasync function processLargeData(items) { for (const item of items) { // Process the item... // Check if we've been running for too long if (performance.now() > deadline) { await new Promise(resolve => setTimeout(resolve, 0)); deadline = performance.now() + 50; // Reset budget } } }
By adding this check, you ensure the browser can pick up user clicks or scroll events, keeping the interface feeling snappy even while heavy lifting happens in the background.
While optimizing the main thread is vital, don't ignore the architecture of your page. Techniques like Next.js Partial Prerendering: Optimizing Dynamic E-commerce Feeds can help by isolating static and dynamic content, which prevents the dynamic parts of your page from blocking the interactions on the static ones.
However, even with the best architecture, you need to watch your bundle size. I’ve seen teams add a 200kb library just to handle date formatting, which then adds 50ms of parse and compile time to the main thread every time the page loads. It’s death by a thousand cuts.
Q: Is INP optimization only about JavaScript execution? A: Not entirely, but it's the biggest factor. Style and layout calculations also run on the main thread. If you trigger a massive layout thrash by changing CSS classes after a click, that also contributes to a high INP.
Q: How do I measure INP in production?
A: Use the web-vitals library to track real user metrics. Lab data in Lighthouse is great for testing, but it doesn't account for the varying hardware speeds of your actual users.
Q: What is the "ideal" INP? A: Google considers anything under 200ms "Good." Anything above 500ms is "Poor." Aim for under 200ms, but prioritize your most critical interactions (like checkout or search) first.
I’m still not 100% happy with our current setup. We still have a few legacy components that are difficult to optimize without a full rewrite. Next time, I’d push for stricter performance budgets on new features much earlier in the sprint.
INP optimization is a marathon, not a sprint. You’ll find yourself constantly balancing the need for rich, interactive features against the physical limits of the browser's main thread. Keep your tasks small, yield often, and always trust the performance profile over your gut feeling.
Measuring performance with tools you trust is the only way to fix real regressions. Stop chasing Lighthouse scores and start tracking actual user metrics.