When a user taps a button and the page seems to freeze for half a second, that delay is captured by Interaction to Next Paint (INP). INP measures the time from when a user interacts (click, tap, keypress) to when the browser paints the next frame. A poor INP frustrates users and hurts your site's Core Web Vitals score. This guide breaks down the most common causes of INP delays and gives you concrete fixes—no fluff, no fake case studies.
Where INP Delays Show Up in Real Work
INP problems rarely appear in isolation. They surface during typical user flows: clicking a search filter, submitting a form, opening a menu, or typing into an input field. In our experience auditing dozens of sites, the worst offenders are single-page applications (SPAs) with heavy JavaScript bundles, but even static sites can suffer if they load third-party scripts that block the main thread.
One common scenario is a product listing page where users apply multiple filters. Each click triggers a re-render of the grid, and if the filter logic runs synchronously on the main thread, the page becomes unresponsive. Another is a checkout flow where clicking 'Place Order' starts a long validation routine that blocks the UI from updating for hundreds of milliseconds.
We've also seen problems on news articles: a user taps a 'Read More' button that expands a section, but the button's click handler does a synchronous DOM query that forces layout recalculation. The result is a visible delay before the content appears. These delays are not just cosmetic—Google's INP threshold is 200 milliseconds for 'good', and anything above 500 ms is 'poor'.
The key insight is that INP is not just about page load; it's about responsiveness throughout the user's session. A fast initial load means little if every click feels sluggish. So where do you start? The first step is to identify which interactions are slow. Use the Performance panel in Chrome DevTools to record interactions and look for long 'Event: click' or 'Function Call' entries. Also check the 'Web Vitals' section in Lighthouse for field data if you have Chrome User Experience Report (CrUX) data.
Common Interaction Types That Trigger INP
Not all interactions are equal. Clicks and taps on buttons, links, and form controls are the most common. Keypress events on search fields or text inputs can also cause delays if the handler does heavy work (like filtering a large list) on every keystroke. Scroll-linked interactions (like sticky headers or lazy-loading images) can contribute indirectly by causing layout shifts that delay the next paint.
Foundations Readers Confuse About INP
There are several misconceptions that trip up even experienced developers. First, many think INP is only about JavaScript execution time. While long tasks are a major factor, INP also depends on how quickly the browser can paint after the event handler finishes. That means layout and paint work—often triggered by CSS changes or DOM manipulation—are just as important.
Second, people often confuse INP with First Input Delay (FID). FID measures only the delay before the event handler starts, ignoring the handler's execution time and the paint afterward. INP captures the full journey: input delay, handler execution, and rendering. So a site with fast FID can still have poor INP if its event handlers are slow.
Third, there's a belief that using requestAnimationFrame or setTimeout automatically fixes responsiveness. In reality, these APIs only defer work; they don't reduce the total amount of work. If you schedule a long task via setTimeout, it still blocks the main thread when it runs. The trick is to break up work into smaller chunks (yielding to the browser) so that the UI stays responsive between chunks.
Fourth, some teams assume that moving work to a Web Worker solves everything. Web Workers run off the main thread, so they don't block the UI. But they can't access the DOM directly. So any DOM manipulation still must happen on the main thread. A better pattern is to use a worker for data processing and send the results back to the main thread for minimal DOM updates.
Finally, many developers overlook the impact of third-party scripts. A single slow analytics script or ad library can delay all interactions if it runs on the main thread. We've seen cases where a third-party chat widget's initialization caused 300 ms of INP delay on button clicks. Profiling with the 'Third-Party' panel in DevTools helps identify these culprits.
Key Metrics to Track
Besides INP itself, watch for Long Tasks (tasks >50 ms) in the Performance panel. Also monitor 'Total Blocking Time' (TBT) as a lab proxy for INP. In the field, CrUX data gives you the real INP percentile. Aim for the 75th percentile under 200 ms.
Patterns That Usually Work
After diagnosing the problem, the next step is applying fixes. Here are patterns we've seen succeed in production.
Yielding to the Browser
The most effective pattern is yielding control back to the browser periodically so it can process pending user interactions and paint updates. Use setTimeout() with a delay of 0, or better, scheduler.yield() if available (Chrome 115+). For example, if you have a loop that processes 1000 items, break it into batches of 50 and yield after each batch:
async function processItems(items) {
for (let i = 0; i < items.length; i += 50) {
const batch = items.slice(i, i + 50);
batch.forEach(processOne);
await new Promise(r => setTimeout(r, 0));
}
}This pattern keeps the main thread free to handle click events between batches. For frameworks like React, consider using useTransition to mark non-urgent state updates as low priority. This lets React interrupt the update if a higher-priority interaction (like a click) comes in.
Debouncing and Throttling
For interactions that fire rapidly—like keypress in a search field—debounce or throttle the handler. Debouncing waits until the user pauses (e.g., 300 ms) before running the handler. Throttling runs the handler at most once per interval (e.g., every 200 ms). This prevents heavy work from executing on every keystroke.
Be careful with throttle: if the user types quickly, the handler runs at most every N ms, but the UI still feels responsive because the next paint happens between throttled calls. Debounce is better for actions that only need to happen once after the user stops (like search suggestions).
Using Passive Event Listeners
For scroll and touch events, add the { passive: true } option to your event listener. This tells the browser that the handler won't call preventDefault(), so the browser can start scrolling immediately without waiting for the handler to finish. This doesn't directly improve INP for clicks, but it reduces main-thread blocking from scroll handlers, freeing resources for other interactions.
Splitting Long Tasks with isInputPending
The navigator.scheduling.isInputPending() API lets you check if the user has pending input (like a click) before continuing a long task. If input is pending, yield immediately. This is more efficient than yielding on a timer because you only yield when necessary.
function processWithYield(items) {
for (const item of items) {
process(item);
if (navigator.scheduling.isInputPending()) {
// Yield to handle pending input
setTimeout(() => processWithYield(remaining), 0);
return;
}
}
}This pattern is especially useful for background tasks like analytics processing or data syncing that don't need to be synchronous.
Anti-Patterns and Why Teams Revert
Even with good intentions, teams often fall back into old habits. Here are anti-patterns to watch for.
Doing Too Much in One Event Handler
It's tempting to cram all logic—validation, API calls, DOM updates—into a single click handler. But this creates a long task that blocks the UI. Instead, separate concerns: validate immediately (lightweight), then defer the API call to a microtask or a separate function that yields. For DOM updates, batch them using a framework's reactivity system rather than manual DOM manipulation.
Why do teams revert? Because separating logic requires refactoring and adds complexity. A single monolithic function is easier to write initially. But the performance cost is real. We've seen projects where a 'quick fix' of adding setTimeout around the whole handler just defers the problem—the long task still runs, just later.
Ignoring Layout Thrashing
Reading and writing to the DOM in alternating order forces the browser to recalculate layout multiple times. For example, reading element.offsetHeight then setting element.style.height then reading again causes forced synchronous layouts. This can add 100+ ms to an interaction. The fix is to batch reads first, then writes. Use a library like FastDOM or manually group operations.
Teams revert because they don't notice layout thrashing in development (where the machine is fast). It only becomes visible on slower devices or under load. Profiling with the 'Rendering' tab in DevTools (enable 'Layout Shift Regions' and 'Paint flashing') helps catch it.
Overusing requestAnimationFrame for Non-Visual Work
requestAnimationFrame (rAF) is designed for visual updates, not general task scheduling. If you schedule a long-running data processing task inside rAF, you delay the next frame and hurt INP. Use rAF only for DOM changes that need to happen before the next paint. For other work, use setTimeout or scheduler.postTask.
Why revert? Because rAF feels like a 'performance API', so developers assume it's always good. But it can backfire if misused.
Maintenance, Drift, and Long-Term Costs
Fixing INP once isn't enough. As your codebase evolves, new features can reintroduce delays. Here's how to keep INP under control.
Setting Up Performance Budgets
Establish a performance budget for INP: for example, no interaction should exceed 100 ms in the lab. Use Lighthouse CI or custom Puppeteer scripts to measure interactions on every pull request. If a PR adds a new event handler that pushes INP over the budget, flag it before merge.
This requires tooling integration, which is a cost. But without a budget, performance drifts silently. We've seen teams spend months optimizing only to have a single third-party script or a junior developer's heavy click handler undo all the gains.
Regular Audits with Real User Monitoring (RUM)
Lab tests (Lighthouse) catch many issues, but field data (CrUX, RUM tools like Web Vitals library) reveals what real users experience. Set up a dashboard that tracks INP over time, segmented by device type and page. If you see a spike, investigate which interaction and which page. Common causes: a new A/B test script, a framework upgrade that changed how events are handled, or a new feature with heavy JavaScript.
The cost is ongoing monitoring and a culture of performance. Teams that treat performance as a one-time project often see INP degrade within months.
Training and Code Reviews
Educate your team about INP and the patterns above. Add performance as a checklist item in code reviews: 'Does this event handler yield? Does it cause layout thrashing? Could it be deferred?' This is a low-cost habit that prevents drift.
When NOT to Use This Approach
The patterns in this guide work for most web apps, but there are situations where they may not apply.
When the Interaction Is Trivial
If your click handler only toggles a class (e.g., opening a dropdown), adding yielding or debouncing adds unnecessary complexity. The overhead of scheduling a microtask can actually make the interaction slower. Only optimize when profiling shows a real delay.
When the Bottleneck Is Network, Not CPU
If INP is high because your page loads a large JavaScript bundle on interaction (e.g., a 'View More' button that fetches a chunk of code), the fix is code splitting, not yielding. The patterns here address main-thread work, not network latency. Use dynamic imports and preload critical resources.
When Using Frameworks with Built-in Scheduling
Modern frameworks like React 18 (with concurrent features) and Vue 3 (with Suspense) already manage yielding and prioritization. Manually adding setTimeout yields inside framework event handlers can conflict with the framework's scheduler. Instead, use the framework's primitives: useTransition in React, nextTick in Vue, or Angular's NgZone.
When the User Base Is Mostly Desktop with Fast Machines
If your analytics show that 95% of users are on high-end desktops with fast CPUs, INP may not be a critical issue. But be careful: even fast machines can suffer from long tasks caused by memory leaks or infinite loops. Check field data before deprioritizing.
Open Questions / FAQ
Q: Does INP affect SEO? Yes, INP is a Core Web Vital and influences search rankings. Google announced that INP replaces FID in March 2024. A poor INP can hurt your visibility in search results.
Q: Can I fix INP without touching JavaScript? Partially. Reducing DOM size, avoiding complex CSS selectors, and using content-visibility: auto for offscreen content can reduce layout and paint work. But most INP issues stem from JavaScript event handlers, so some JS changes are usually needed.
Q: How do I measure INP in the field? Use the web-vitals library from Google. It reports INP values for real users. You can send them to Google Analytics, RUM tools, or your own backend. Also check CrUX in PageSpeed Insights.
Q: What's the difference between INP and FID? FID measures only the delay before the event handler starts (input delay). INP measures the entire interaction latency: input delay + handler execution + rendering. INP is a more complete metric.
Q: Should I use Web Workers for all heavy logic? Web Workers are great for pure computation (e.g., data processing, encryption) but cannot access the DOM. For DOM-heavy interactions, you still need to optimize main-thread work. Use workers in combination with yielding.
Q: How do I handle third-party scripts that cause INP? Load them asynchronously with async or defer. If they block interactions, consider lazy-loading them after user interaction or moving them to a Web Worker (if possible). Some third-party scripts offer lightweight alternatives (e.g., Google Analytics's gtag.js can be loaded with async).
Summary and Next Experiments
Improving INP is about making your site feel responsive. The core strategies are: yield to the browser, break up long tasks, avoid layout thrashing, and monitor both lab and field data. Start by profiling your most common interactions—search, filter, submit—and apply the patterns that fit your stack.
Next steps to try:
- Profile a slow interaction in Chrome DevTools and identify the longest task. Use
scheduler.yield()orsetTimeoutto break it up. - Add a performance budget for INP in your CI pipeline using Lighthouse CI.
- Set up RUM tracking with the web-vitals library and create a dashboard to monitor INP over time.
- Conduct a code review of your top 5 event handlers and refactor any that do too much synchronously.
- Experiment with
isInputPendingin a background task to see if it reduces perceived lag.
Remember, there's no silver bullet. Each site has unique constraints. But by systematically applying these fixes and avoiding the common pitfalls, you can bring your INP into the green and keep it there. Start with one interaction, measure the improvement, and iterate.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!