Interaction to Next Paint became a Core Web Vital in March 2024, replacing First Input Delay. The change matters because INP measures something FID missed: how snappy the page feels every time the user interacts, not just on the first tap.
In 2026, roughly 35% of mobile pages and 25% of desktop pages fail INP. Most sites that pass LCP and CLS still fail INP. It's the hardest Core Web Vital to fix because it's about JavaScript execution, not about loading.
FID measured only the first interaction, and only the input-delay portion (the time between the user tapping and the browser starting to handle the tap). It missed two things that matter to users:
Subsequent interactions. A page that's snappy on the first tap and janky on the tenth is a frustrating page. FID gave it a passing score.
Processing and rendering time. FID stopped measuring as soon as the browser dispatched the event. The time spent running the actual handler — often hundreds of milliseconds — was invisible.
INP measures the worst (or near-worst, depending on session length) full interaction round-trip across the entire page session. It's slower-feeling pages, in a number. The honest metric.
INP cannot be reliably measured synthetically. Synthetic tests don't perform 50 interactions across a typical session; they perform one or two scripted ones. Real-user monitoring is the only credible source.
Three measurement options:
Google Search Console Core Web Vitals report. Field data from real Chrome users on real connections. Lag is 28 days. This is your source of truth.
Vercel Speed Insights / Cloudflare Web Analytics / SpeedCurve RUM. Real-time field data, broken down by route, device, and connection type. Higher signal than GSC for diagnosis.
PageSpeed Insights "Field Data" tab. Same data as GSC, accessible per-URL. Useful for spot-checking specific pages.
If you only have synthetic INP scores (from PSI's Lab tab or WebPageTest), treat them as directionally useful and not as ground truth. The user's actual experience is in the field data.
The Page Speed Grader flags the specific elements and event types where INP is high — "click on <button> in .product-card took 412ms". That's the diagnostic to start from.
The categories where slow INP almost always lives:
Hydration on first interaction. A React/Vue page where the framework is still hydrating when the user taps something. The first click is queued behind framework startup work.
Heavy event handlers. A click handler that runs 200ms of JavaScript before the browser can render the response. Common in apps that update large client-side state on every interaction.
Third-party scripts. Analytics, A/B testing, chat widgets, ad scripts — each adds tasks to the main thread, blocking interaction handling.
Layout thrashing. Event handlers that read and write the DOM in a pattern that forces multiple synchronous layouts. Especially common in carousels, accordions, and infinite-scroll lists.
The main thread can only do one thing at a time. A task that takes 200ms to execute blocks every interaction for 200ms. INP is dominated by long tasks.
Identify them: in Chrome DevTools' Performance panel, recorded long tasks (>50ms) are visible in pink-bordered blocks on the main thread.
Two techniques to break them up:
scheduler.yield() (modern browsers). Inside a long task, call await scheduler.yield() to give the browser a chance to render and handle other inputs:
async function doExpensiveWork(items) { for (const item of items) { process(item); if (scheduler?.yield) await scheduler.yield(); }}
requestIdleCallback for non-urgent work. If the task isn't directly tied to a user interaction (analytics flushing, background fetching, log buffering), schedule it for browser idle time:
For React-based apps, React 18's useTransition and useDeferredValue hooks let the framework yield naturally — most state updates that don't need to be immediate should be wrapped in startTransition.
Less JavaScript means faster hydration, fewer long tasks, faster startup of third-party scripts. The mechanics:
Tree-shake aggressively. Modern bundlers (Vite, Webpack 5, Turbopack) tree-shake by default if your imports are ES modules. Check the bundle analyzer to confirm large libraries are actually being tree-shaken.
Code-split routes and heavy components. A React app that ships all routes in a single bundle hydrates slower than the same app with one bundle per route. Next.js does this automatically; SPAs need React.lazy() or equivalent.
Replace heavy libraries. Moment.js (300KB) -> date-fns (modular, 5KB per function). Lodash (full) -> lodash-es with named imports. Recharts -> a custom SVG for simple charts. Each swap is meaningful.
A common 2026 quick win: server components. Next.js App Router server components don't ship JavaScript to the client at all for the parts of the UI that don't need interactivity. Migrating a page's static sections from client to server components often cuts the route's JS bundle by 60–80%, which directly improves INP.
Third-party scripts are responsible for a meaningful share of INP failures. Each script that loads, executes, or runs handlers competes with your application code for main-thread time.
The triage:
Audit what's actually used. Most sites have 8–15 third-party scripts. Half of them aren't actively used (forgotten A/B tests, abandoned chat widgets, old analytics that nobody reads). Remove anything unused.
Defer non-critical scripts.<script defer> for anything that doesn't need to run before page render. <script async> for scripts that can run any time after parsing.
Use Partytown for heavy third-parties. Partytown runs third-party scripts in a Web Worker, off the main thread. Google Tag Manager, Facebook Pixel, Hotjar, and similar are common Partytown candidates. The setup is one config file; the INP improvement is often 100ms+.
For event handlers that are unavoidably doing real work, the patterns that help:
Debounce or throttle. A scroll handler that fires 60 times a second and updates state on each fire will tank INP. Debounce to 100ms; throttle to 16ms (one animation frame).
Avoid synchronous layout reads. Reading element.offsetWidth inside a handler that also writes to the DOM forces the browser to synchronously layout. Group reads first, then writes. Or use ResizeObserver / IntersectionObserver for layout-dependent logic.
Move state updates out of the critical path. If a click handler needs to call an analytics function and update some UI state, do the UI update first (synchronously) and the analytics call after (asynchronously, deferred):
The setTimeout(fn, 0) pattern yields to the browser before the analytics call runs, which can mean the difference between a 60ms interaction and a 220ms interaction.
The single most common INP failure isn't in the application code — it's in the third-party scripts loaded synchronously in the document head. A Google Tag Manager snippet that loads four analytics tools, two heatmaps, and one chat widget can add 300–500ms to every interaction during the first 30 seconds of page load.
The fix that disproportionately helps: load GTM with defer (or via Partytown), and audit which tags are actually firing. Most container-managed sites have 60–70% wasted tag firings — tags that have outlived their experiment, tags duplicated across two analytics systems, tags loading on every page when they should only fire on conversions.
Cutting GTM payload alone has cleared INP on dozens of client sites we've worked with. It's worth one full afternoon of effort.
INP measures the slowest interaction, not the average. A page can pass INP at 180ms and still feel sluggish if every interaction is at 170ms. The user perceives consistency, not just worst-case.
Aim for median INP under 100ms in addition to passing the 200ms threshold. Faster interactions feel snappier; the field data will reward it with better engagement metrics, which over time reward it with better ranking.