You click a button. Nothing happens for half a second. The page feels sluggish, and you wonder if the site is broken. That delay is exactly what INP (Interaction to Next Paint) measures—the time from a user gesture to the next visual update. For developers, a poor INP score is often the result of a few repeated mistakes. In this guide, we walk through the top three errors that destroy responsiveness and show you how to fix each one. By the end, you'll have a clear workflow to diagnose and repair INP issues in your own projects.
1. Who Needs This and What Goes Wrong Without It
Every web developer who cares about user experience needs to understand INP. It is one of Google's Core Web Vitals, and starting in March 2024, it replaced First Input Delay (FID) as a ranking factor. But more importantly, a bad INP directly frustrates users. Imagine a checkout form where pressing 'Submit' freezes the screen for 800 milliseconds—users may retry, double-submit, or abandon the cart entirely.
Without deliberate fixes, INP commonly exceeds 300 milliseconds on real-user devices, especially on mobile. The top three mistakes are: (1) running long synchronous JavaScript on the main thread, (2) using event delegation without guarding against heavy handlers, and (3) loading third-party scripts that block input processing. Let's look at each in turn, but first, understand what goes wrong when these mistakes are left unchecked.
When the main thread is blocked, the browser cannot respond to clicks, scrolls, or key presses until the current task finishes. A single long task of 100 milliseconds can already feel slow. Worse, if multiple long tasks queue up, the input delay compounds. Event delegation adds another layer: a click on a deeply nested element triggers a listener on a parent, which may run expensive DOM queries or state updates for every click site-wide. Third-party scripts—like analytics, ads, or chat widgets—often load and execute on the main thread, competing for the user's input time.
The result is a sluggish interface that users perceive as broken. On mobile devices with slower CPUs, these problems magnify. A site that scores well on a desktop may still fail INP on a mid-range phone. The fix requires targeted changes, not a blanket rewrite.
What a typical failure looks like
Consider a news site with a sticky header menu. The menu uses a single click handler on the document to open submenus. That handler also runs analytics tracking and lazy-loads images. On a slow connection, clicking a menu item triggers a chain: the click event fires, the handler runs a fetch for analytics, then updates the DOM for the submenu, then checks for new images. All of this happens synchronously on the main thread. The user sees no visual feedback for 400 milliseconds. This pattern repeats across many sites and is fixable with the techniques we describe later.
2. Prerequisites and Context Readers Should Settle First
Before diving into fixes, you need a baseline understanding of how the browser processes events. The event loop handles tasks in a queue: user interactions, rendering, JavaScript execution, and network callbacks. INP measures the time from the input event being queued to the next paint that reflects the interaction's result. This includes the time to run event handlers, perform layout, and paint.
You should also be familiar with the Performance API. Modern browsers expose performance.now() and the PerformanceObserver interface to measure long tasks and event timing. Install the web-vitals library or use Chrome DevTools' Performance panel to record INP. We recommend setting up field data collection via the navigator.webdriver check to avoid skewing results.
Another prerequisite is a realistic testing environment. Emulate a mid-range mobile device (e.g., Moto G4) and throttle CPU to 4x slowdown in DevTools. Test on actual hardware if possible—a cheap Android phone reveals issues that even emulation misses. Also, ensure your site uses HTTPS, as some performance APIs require a secure context.
Know your baseline
Measure your current INP using the onINP() function from the web-vitals library. Log the value for the 75th percentile of interactions. If it's above 200 milliseconds, you have room for improvement. The goal is under 200 ms, with a stretch target of 100 ms for highly interactive apps. Document the worst interactions—often they are clicks on buttons that trigger complex state updates.
Identify your technology stack
The fixes differ for static sites vs. single-page apps (SPAs). In an SPA, event handlers are often bound to components and may trigger re-renders. In a static site, event delegation is common, and third-party scripts are the main culprit. Understanding your stack helps you choose the right approach. For example, React developers should look at useTransition and useDeferredValue to mark non-urgent updates.
3. Core Workflow: Fixing the Three Mistakes Step by Step
Here is the sequential workflow we recommend for addressing the top three INP mistakes. Follow these steps in order, testing after each change to see the impact.
Step 1: Break up long tasks
Identify long tasks (tasks over 50 ms) using the Long Tasks API or DevTools' Performance panel. Common sources are large synchronous loops, complex DOM manipulations, and heavy JSON parsing. To fix them, use setTimeout() or requestAnimationFrame() to yield to the event loop. For example, if you loop over 10,000 items and update the DOM each iteration, batch the updates and use requestAnimationFrame() to spread them across frames. Alternatively, use setTimeout(..., 0) to defer non-critical work after the current interaction completes.
For CPU-intensive computations, move them to a Web Worker. This offloads work from the main thread entirely. A Web Worker can process data, parse JSON, or run calculations without blocking user input. The worker communicates back via postMessage(). This is especially useful for image processing or data filtering in dashboards.
Step 2: Optimize event delegation
Event delegation is efficient for binding fewer listeners, but it can backfire if the delegated handler does too much. Audit your delegated handlers: are they querying the DOM with closest() or matches()? Are they running the same logic for every click, even clicks on non-target elements? Add early returns to skip irrelevant events. For example, if you have a delegated click handler on document for buttons with class .menu-item, check event.target.closest('.menu-item') and return immediately if null.
Also, avoid running expensive operations like getBoundingClientRect() or scrollTop inside event handlers. Cache layout values outside the handler. If the handler updates state that triggers a re-render (in a framework), consider batching state updates or using a scheduler like requestIdleCallback().
Step 3: Manage third-party scripts
Third-party scripts are often loaded synchronously or with async but still execute on the main thread. Use the defer attribute for scripts that do not need to block rendering. For critical third-party services (like analytics), load them after user interaction using setTimeout() or the IntersectionObserver to delay until the user has engaged. Alternatively, use fetch() to preload the script as a blob and execute it later, though this requires careful handling of cross-origin restrictions.
Another technique is to use document.createElement('script') and set async = true, then append it to the DOM after the page's main content is interactive. This defers the script's execution until after the initial render and user interactions. For non-essential widgets (like chat), consider lazy-loading them only when the user scrolls near them.
4. Tools, Setup, and Environment Realities
You need the right tools to diagnose and verify fixes. Chrome DevTools is the primary environment, but also test in Firefox and Safari for cross-browser differences. The Performance panel in Chrome shows a flame chart of tasks, long tasks highlighted in red, and input events marked with a lightning bolt icon. Use the 'Web Vitals' overlay in DevTools to see real-time INP scores.
For programmatic monitoring, use the Event Timing API. Register a PerformanceObserver for 'first-input' and 'event' entries. This gives you the duration of each interaction. Combine this with PerformanceObserver for 'longtask' entries to see which tasks caused delays. Example:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'longtask') {
console.log('Long task:', entry.duration);
}
}
});
observer.observe({ type: 'longtask', buffered: true });Set up continuous monitoring in production using the web-vitals library and send data to an analytics endpoint. This helps you catch regressions after deployments. Also, use Lighthouse in CI to enforce an INP budget. A budget of 200 ms for mobile is a good starting point.
Environment realities
Not all fixes work in every environment. Web Workers are not available in all contexts (e.g., shared workers have limited support). Third-party script optimization is constrained by the provider's loading behavior—some scripts cannot be deferred without breaking functionality. In those cases, you may need to negotiate with the vendor or replace the service. Also, note that browser extensions and developer tools can affect performance measurements; always test in a clean profile.
Mobile networks add latency. Even if your main thread is free, a slow network fetch for a resource triggered by a click will delay the next paint. Prefetch critical resources or use server-side rendering to minimize client-side work.
5. Variations for Different Constraints
The optimal approach depends on your project's constraints. Here are three common scenarios and how to adjust the workflow.
Scenario A: Legacy codebase with heavy jQuery
If you maintain a site built with jQuery, event delegation is likely everywhere. Start by profiling which delegated handlers are slow. Often, handlers use $(this).closest('.widget') which traverses the DOM. Replace these with direct event binding on the closest static parent. Also, replace $.each() loops with native forEach() and batch DOM updates with a document fragment. For long tasks, use setTimeout() to break up loops. Avoid using $(document).ready() for heavy initialization; move that to after the first paint.
Scenario B: React SPA with frequent state updates
In React, the main thread is often blocked by reconciliation and re-renders. Use React.memo and useMemo to avoid unnecessary re-renders. For interactions that trigger large state changes (e.g., opening a modal with a list), wrap the state update in startTransition to mark it as non-urgent. This allows React to interrupt the render for higher-priority input. Also, consider using useDeferredValue to defer updating a part of the UI until after the interaction. Profile with React DevTools to see component render times.
Scenario C: Content site with many third-party scripts
For sites that rely on ads, analytics, and social widgets, the main issue is script execution. Use a tag manager like Google Tag Manager, but audit the tags that fire on click events. Many tags run custom JavaScript that blocks the main thread. Set up consent management to delay non-essential scripts until after user interaction. For ads, use lazy loading with IntersectionObserver and ensure ad scripts are loaded with async. If a third-party script is causing long tasks, consider removing it or finding a lighter alternative.
6. Pitfalls, Debugging, and What to Check When It Fails
Even with the right fixes, things can go wrong. Here are common pitfalls and how to debug them.
Pitfall: Over-optimizing with micro-tasks
Breaking a long task into many tiny tasks can actually hurt performance if each task yields to the event loop. Yielding itself has overhead. Use requestAnimationFrame() for visual updates and setTimeout(..., 0) for non-visual work, but avoid yielding excessively. A good rule is to yield only when a task exceeds 50 ms. Use the Long Tasks API to confirm that your changes reduce long task duration without increasing the total number of tasks too much.
Pitfall: Ignoring passive event listeners
Touch and wheel events that are not declared as passive can block scrolling. Add { passive: true } to event listeners for touchstart, touchmove, wheel, and mousewheel unless you need to call preventDefault(). This tells the browser that the listener will not cancel the event, allowing the browser to scroll immediately. For click events, passive is not relevant, but for scroll-linked interactions, it matters.
Pitfall: Third-party script that re-attaches listeners
Some third-party scripts add their own event listeners after you have optimized yours. Use DevTools' Event Listener Breakpoints to see which code is adding listeners. If a third-party script adds a slow listener to the document, consider wrapping it in a setTimeout() to defer its registration until after the page is interactive. Alternatively, use AbortController to remove the listener if it becomes problematic.
Debugging checklist
- Check if the interaction is blocked by a long task. Look at the Performance flame chart for a red block before the input event.
- Verify that event handlers are not re-triggering layout. Use DevTools' 'Rendering' tab to enable 'Layout Shift Regions'.
- Test with third-party scripts disabled to see if they are the cause. Use Chrome's network request blocking to block specific scripts.
- Measure on real devices using field data. Lab tests can miss variations in CPU throttling.
7. FAQ and Checklist for INP Optimization
This section answers common questions and provides a concise checklist to apply on your next project.
FAQ
How do I know if my INP is caused by JavaScript or by rendering? In the Performance panel, check the 'Summary' tab. If the majority of time is in 'Scripting', it's JavaScript. If 'Rendering' or 'Painting' dominates, focus on reducing layout thrash and paint complexity.
Should I use debouncing or throttling for event handlers? Use throttling for events that fire continuously (like scroll or resize) and debouncing for events that fire once after a pause (like keyup in a search box). For click events, neither is usually needed unless the handler runs expensive logic.
Can I fix INP without changing third-party scripts? Partially. You can defer their execution, but if a script is inherently slow, you may need to replace it. Try loading it after user interaction first.
Checklist
- Measure baseline INP with web-vitals library on real devices.
- Identify the top three interactions that cause the worst INP.
- Break up any long task over 50 ms using
setTimeout()or Web Workers. - Optimize delegated event handlers: early returns, cache DOM queries, avoid forced layouts.
- Defer third-party scripts that are not needed for initial interaction.
- Add passive event listeners for scroll and touch events.
- Test each change individually with DevTools Performance panel.
- Monitor INP in production with real-user monitoring (RUM).
8. What to Do Next
Start by auditing your current INP using the steps in section 2. If you find a specific interaction that is slow, apply the fix for the corresponding mistake. For example, if a click on a 'Load More' button freezes the page, it's likely a long task from fetching and rendering data. Break that fetch into chunks using requestAnimationFrame().
Next, set up a performance budget in your CI pipeline. Use Lighthouse CI to fail a build if the INP score drops below 0.8 (or the equivalent lab value). This prevents regressions from being merged. Also, share the checklist from section 7 with your team so everyone understands the common pitfalls.
Finally, consider conducting a performance audit every quarter. Web standards and browser capabilities evolve, and what worked six months ago may need adjustment. Test with new browser versions and device profiles. If you maintain a public site, publish a case study of your improvements—it builds trust with users and demonstrates your commitment to quality.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!