
Lighthouse said 98. Your synthetic tests said the site was fast. Your TTFB dashboard was green. And then Google Search Console quietly showed your "Poor INP" page count climbing to 60% of your top URLs, your rankings slid two months in a row, and the team finally noticed something was wrong.
This is the INP era. Since March 2024, Interaction to Next Paint replaced First Input Delay (FID) as a Core Web Vital. Where FID measured only the first interaction's delay — and only the delay part, not the work that followed — INP measures the worst of all interactions across an entire visit, including the time spent updating the DOM and painting the result. It is far harder to game, far closer to actual user perception, and it is part of the SEO ranking signal Google uses on every page it indexes.
The catch: INP is fundamentally a field metric, not a lab one. Lighthouse can simulate it, but the number Google ranks on comes from the Chrome User Experience Report (CrUX) — real users, in real browsers, on real devices, over a rolling 28-day window. You cannot fix INP by optimising your staging environment. You fix it by measuring real-user interactions in production, finding the slow ones, and shipping fixes per-route until the 75th percentile gets back under 200ms.
This guide is the production-monitoring layer for INP: what it measures, how to instrument it, what to alert on, and the specific patterns that wreck it — autocomplete handlers, virtualised lists, third-party tag managers, heavy onClicks, framework reactivity costs. By the end you will have a per-route, per-device, per-release view of INP that catches regressions in deploys, not in next month's Search Console.
What INP Actually Measures
INP is the worst-of-N interaction latency across a page visit, taking the long tail seriously instead of averaging it away.
Mechanically, for every qualifying interaction (click, tap, key press — not scroll, not hover) the browser measures:
- Input delay — time from user input to event handler starting
- Processing time — time spent in the event handler (your JavaScript)
- Presentation delay — time from handler finishing to the next frame painted
INP = the highest of these totals seen on the page during the visit (with a slight smoothing for pages with > 50 interactions, where Google uses the 98th percentile of interactions).
This is fundamentally different from FID:
| FID | INP | |
|---|---|---|
| Measures | First interaction only | All interactions, worst-of-N |
| Time scope | Input delay only | Input delay + handler + presentation |
| Failure mode caught | Long task during page load | Any slow interaction in the visit |
| Easy to game | Yes (skip the first click) | No |
If your app feels fine for the first click but stutters on the third drag-and-drop — FID was green, INP is red.
The Google thresholds
| Bucket | INP value (p75 across CrUX dataset) |
|---|---|
| Good | < 200ms |
| Needs improvement | 200ms – 500ms |
| Poor | > 500ms |
Google's ranking signal uses your site's 75th-percentile INP from CrUX over a rolling 28-day window, per URL group. A single bad route can move your site-wide CrUX number into "Poor" because the dataset isn't averaged across pages.
Lab vs Field — Why INP Is a Field-Only Metric
Most performance metrics have meaningful lab equivalents. INP doesn't, for three structural reasons:
- No interaction in lab tests by default. Lighthouse simulates only page load. No clicks, no typing, no real interaction.
- Per-visit, not per-page. INP captures the worst across the entire user journey, including interactions that happen 30 seconds after load.
- Device + connection + extension reality. A typical CrUX visitor is on a 4-year-old Android with three extensions and an ad blocker. Your M3 MacBook tells you nothing about their experience.
Tools that can approximate INP in lab:
- Lighthouse User Flows — you script a flow with clicks and the analyzer reports interaction latency per step. Useful for catching regressions in CI; not what Google ranks on.
- WebPageTest's scripted test runs with synthetic interactions.
- DevTools Performance Insights panel — replays interactions and shows their breakdown.
But the actual INP number Google uses you only get from real users. So the rest of this guide is about RUM (Real User Monitoring).
Instrumenting INP With web-vitals.js
The official, low-overhead, ~3KB library that owns every Core Web Vital metric:
npm install web-vitals
Minimal collector:
import { onINP } from 'web-vitals';
onINP((metric) => {
const payload = {
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
navigationType: metric.navigationType,
eventTarget: metric.attribution?.interactionTarget,
eventType: metric.attribution?.interactionType,
loadState: metric.attribution?.loadState,
path: window.location.pathname,
deviceMemory: navigator.deviceMemory,
connection: navigator.connection?.effectiveType,
release: window.__APP_VERSION__,
};
navigator.sendBeacon('/api/rum/inp', JSON.stringify(payload));
});
A few important details:
- Use
sendBeacon, notfetch, so the metric ships even if the user closes the tab - Include
attribution—web-vitalsv4+ adds ametric.attributionobject withinteractionTarget(CSS selector of the element) andinteractionType(pointer,keyboard, etc). This is how you find which button is slow. - Include
loadState— distinguishes page-load INP (during initial render) from steady-state INP (later interactions) - Include device and connection info — INP per device class is non-negotiable for diagnosis
- Include the release/build version — so you can attribute regressions to deploys
What attribution gives you
The attribution object on onINP is the single most useful piece of telemetry for fixing INP. It includes:
interactionTarget— CSS selector of the element that fired the interactioninteractionType—pointer,keyboard, etcinputDelay— phase 1processingDuration— phase 2 (your JS)presentationDelay— phase 3nextPaintTime— absolute timestamp
Without attribution, INP is just a number on a chart. With it, you can answer "which button is slow on which page on which device?" — which is the only question that leads to a fix.
CrUX, the 28-Day Window, and Why Your Fix Is Slow to Show
Google's ranking signal uses CrUX data:
- Aggregated from real Chrome users (with appropriate opt-in)
- 75th percentile of INP per URL group
- 28-day rolling window
- Updated daily
The 28-day window is what trips most teams up. You ship the fix on day 1. By day 7, your own RUM shows the metric back in "Good". But the CrUX number — the one in Search Console — still has 21 days of old data in the average. The Search Console card stays red. People assume the fix didn't work and revert it.
It did work. The CrUX number lags by roughly 28 days from the moment the change reaches 100% of users. Two operational implications:
- Don't trust Search Console for fix-confirmation. Trust your own RUM, with day-1 resolution.
- Don't roll back a fix because Search Console hasn't moved. Wait at least 14 days post 100% rollout before judging.
Most teams that "tried INP fixes that didn't work" simply rolled them back too early.
Per-Route INP — The Most Important Cut
INP is reported per URL group in CrUX. A single bad route — your /dashboard, your /checkout, your search results — can drag the site-wide aggregate into "Poor" even though every other route is fine.
Required cuts in your RUM:
- Per route (after route-pattern normalization —
/users/12345rolls up to/users/:id) - Per device class — desktop, low-end mobile, high-end mobile (use
navigator.deviceMemory) - Per connection type —
4g,3g,slow-2gfromnavigator.connection.effectiveType - Per geo — INP for Tier-3 markets is structurally worse; surface this so you don't panic-fix on data from one region
- Per release — version tag on every metric so you can spot regressions tied to specific deploys
The dashboard layout that works:
| Route | p75 INP | Sample count | Worst element |
|---|---|---|---|
/dashboard |
480ms | 12,400 | button.refresh |
/checkout |
220ms | 3,200 | — |
/search |
180ms | 9,100 | — |
/ |
140ms | 22,000 | — |
A glance tells you: /dashboard is your problem, and the refresh button is where the time is going. Without per-route cuts you're chasing aggregate numbers and getting nowhere.
The INP Killers (And Where They Hide)
The patterns we see take down INP, in rough order of frequency:
1) Autocomplete / search-as-you-type handlers
Every keystroke fires a handler. If the handler is unthrottled, hits the DOM, or kicks off a fetch that updates a list, every keystroke is a fresh INP measurement. Bad implementations regularly clock 400-800ms INP on /search pages.
Fix: debounce input handlers (150-250ms is usually right), do work off the input event with requestIdleCallback, render results in a virtualised list, never reflow the entire results list on each keystroke.
2) Heavy onClick handlers in lists
A list of 200 items where each row's click handler computes derived state, hydrates an editor, or triggers a Suspense boundary. Each click is 300-500ms because the handler closes over the parent component's render cost.
Fix: split the handler — synchronous part does immediate visual feedback (selected state, focus ring), then requestAnimationFrame or scheduler.yield() runs the heavy work after the paint. The user perceives the click as instant even though the underlying work takes 200ms.
3) Virtualised lists with expensive items
Virtualisation is supposed to help INP. It does — for scroll. But when virtualisation triggers re-renders on click (because clicked items get a different render path), the cost is paid up front per click. Especially bad with react-window, @tanstack/virtual, and equivalents.
Fix: keep clicked-item state out of the render tree; memoize aggressively; avoid full list re-renders on selection.
4) Tag managers and third-party scripts
Google Tag Manager, Segment, Hotjar, Optimizely — these install long-running event listeners that fire on every interaction. A single misbehaving tag adds 50-200ms input delay to every click on the page.
Fix: audit tags; defer non-critical tags; use Partytown (or its equivalents) to move third-party JS to a Web Worker; remove tags whose ROI doesn't justify the INP cost.
5) Framework reactivity costs
- React — large component trees with cheap-but-many state updates can be slow even with concurrent rendering.
useTransitionandstartTransitionare the tools to know. - Vue 3 — reactive ref/computed cascades can fire more than expected on a click;
shallowRef,markRaw, andtriggerRefhelp. - Svelte 5 — runes are usually faster, but
$effectchains that fire on derived state can still spike. - Angular — Signals (since 16) significantly cut change-detection cost, but legacy zone.js + heavy components still cause INP issues.
The framework rarely is the problem, but framework idiom — and lack of awareness of useTransition / triggerRef / effect-discipline — often is.
6) Page-load INP
INP captures interactions during initial load too. Clicks while the page is still hydrating can take 1-2 seconds. The browser was busy parsing/executing JS when the click happened; the click went in the queue and waited.
Fix: ship less JS at the start; use partial hydration (Astro Islands, Next.js Partial Prerendering, Qwik); deliberately defer non-critical interaction handlers.
7) Long tasks and main-thread monopoly
Tasks > 50ms on the main thread block input handling. The browser's Long Animation Frames API (LoAF, Chrome 123+) shows you these in production with attribution.
Fix: chunk work; scheduler.yield() (Chrome 129+) or await new Promise(r => setTimeout(r, 0)) to yield back to the browser between chunks; move heavy computation to a Web Worker.
For the broader frontend-perf context see Core Web Vitals Performance Monitoring.
Page-Load INP vs Steady-State Interaction INP
The loadState field on the INP attribution object splits these:
loading— interaction happened while the page was still parsing HTMLdom-interactive— DOM parsed, scripts running, page not interactivedom-content-loaded— DCL fired, page hydratingcomplete— fully loaded
Page-load INP (the first three states) tends to be 2-5× worse than steady-state INP. Optimising the wrong one wastes engineering time. Track them separately:
- If page-load INP is your problem → ship less JS at load, defer hydration
- If steady-state INP is your problem → fix the slow handlers per route
Sampling and Cost
Shipping every INP measurement to your backend works at low traffic but blows up fast. At 1,000 INP measurements per minute, your /api/rum/inp endpoint becomes a non-trivial workload.
Strategies:
- Sample at the source — only send if INP > 100ms (you don't need the fast cases for diagnosis, only for percentile calculation)
- Sample by session — pick 10% of sessions and record every INP from them; you get full session context plus statistical reach
- Aggregate at the browser — for the percentile calculation, ship counts in 50ms buckets rather than individual measurements
- Beacon endpoint should be cheap — write to a queue (SQS, Pub/Sub, Workers Analytics Engine), batch into your warehouse later; never block on synchronous DB writes
See API Rate Limit Monitoring: 429 Errors and Throttling for the broader endpoint-protection patterns.
Tooling Landscape
Commercial RUM that includes INP out of the box (mid-2026):
- Vercel Speed Insights — great for Next.js apps on Vercel; per-route breakdown; included in many plans
- Cloudflare Web Analytics + Browser Insights — free, lightweight, p75 INP per route
- Datadog RUM — full session replay, INP attribution, per-release cohort comparison
- New Relic Browser — INP + long-task attribution; strong release-cohort analytics
- Sentry Performance — INP + correlated frontend errors; the link from "this user had bad INP" to "they also got a JS error here" is unique
- Honeycomb + custom OpenTelemetry frontend SDK — full flexibility, more setup
- Akamai mPulse, SpeedCurve — enterprise RUM with strong INP tooling
- PageSpeed Insights (free) — surfaces field CrUX data for any public URL, including INP breakdown per page
DIY:
web-vitalslibrary + beacon endpoint + warehouse (BigQuery / ClickHouse / Snowflake) + dashboard (Grafana / Metabase / Cube)
Most teams start with PageSpeed Insights for awareness, ship web-vitals + beacon for production tracking, and upgrade to commercial RUM when the team needs session-level attribution. See Synthetic Monitoring vs Real User Monitoring for the broader RUM tradeoffs.
Connecting INP Regression to Deploys
The fastest INP debugging trick: bucket every INP measurement by the deploy/release version, then compare cohorts after each release.
const payload = {
// … standard fields …
release: window.__APP_VERSION__, // injected at build time
};
In your dashboard, plot p75 INP for the last 4 releases as side-by-side cohorts. A regression sticks out immediately:
release p75 INP sessions
v3.41.0 180ms 20,400
v3.42.0 192ms 19,800
v3.43.0 420ms 15,200 ← regression starts here
v3.44.0 430ms 8,100
This works far better than alerting on aggregate INP because rolling 24h includes traffic on the old version too — the regression takes a day or two to become statistically visible if you don't bucket by release.
Combine with feature flags: bucket by flag-enabled vs flag-disabled at the same release. Catch regressions tied to flag rollouts, not just code deploys.
Alerting Thresholds That Work
INP regressions move slowly relative to typical monitoring alerts. The thresholds:
Critical (page)
- p75 INP per route > 500ms ("Poor") for 24 hours
- Any single route with p75 INP > 800ms
High (notification)
- p75 INP per route > 200ms ("Needs improvement") for 48 hours
- p75 INP shifted from "Good" to "Needs improvement" between two releases (release-cohort regression)
- Sample count dropping > 30% on a previously-tracked route (instrumentation broke)
Informational
- Worst-element CSS-selector frequency changes (new INP killer appearing)
- Per-device-class p75 INP > 2× the desktop number (mobile-specific regression)
See Alert Fatigue: Notifications That Get Acted On for the broader low-noise alerting principles.
INP and the Adjacent Frontend Pieces
INP is one of three Core Web Vitals. The full picture needs the other two:
- LCP (Largest Contentful Paint) — initial-load perception; covered alongside CLS in Core Web Vitals Performance Monitoring
- CLS (Cumulative Layout Shift) — visual stability
- TTFB — server-side foundation, what comes before LCP and INP; see TTFB Monitoring: Server Response Time
- SPA-specific concerns — soft-navigation INP, router-driven interactions; see SPA Monitoring: Single-Page Application Uptime
- Framework-specific — for Next.js see Next.js Monitoring: Production App Uptime
- Multi-region INP — users in different regions get different INP for the same page; see Multi-Region Monitoring
- Why this matters to revenue — see The Hidden Cost of Slow Websites
INP Monitoring Checklist
-
web-vitalslibrary installed;onINPwith attribution enabled - Beacon endpoint receiving INP measurements with route, device, connection, release tags
- Beacon endpoint is async (queue / Workers Analytics Engine / SQS) — never blocks page unload
- Sampling strategy in place (don't ship every measurement at high traffic)
- Per-route p75 INP dashboard
- Per-device-class and per-connection-type cuts
- Per-release cohort comparison
-
interactionTarget(CSS selector) captured for slowest interactions - Page-load vs steady-state INP tracked separately via
loadState - Search Console "Poor URLs" report subscribed (lagging indicator only)
- PageSpeed Insights checks scripted on top URLs (free CrUX surface)
- Alerting on p75 INP > 200ms / > 500ms per route
- Alerting on cross-release cohort regression
- Third-party tag/script audit done; budgets enforced
- Long Animation Frames API (LoAF) data captured in modern browsers
- No rollback of a fix before 14 days of post-100%-rollout data
How Webalert Helps With INP Monitoring
Webalert handles the external monitoring layer that complements your in-page RUM:
- HTTP monitoring — Watch your
/api/rum/inpbeacon endpoint; alert when it goes 5xx (you're losing measurements silently) - Content validation — Hit your internal
/internal/inp-healthendpoint that surfaces the latest p75 INP per top route; alert when any route crosses 200ms / 500ms - Multi-region checks — INP is region-dependent; external checks from each region confirm reachability + TTFB feeding the metric
- Response time monitoring — Catch TTFB climbing, which directly increases INP for early-load interactions
- Status page — Communicate "site is slow on mobile networks today" to users when CrUX shifts
- Multi-channel alerts — Email, SMS, Slack, Discord, Microsoft Teams, webhooks
- 1-minute check intervals — Detect beacon endpoint outages within 60 seconds
- 5-minute setup — Add endpoints, set thresholds, done
See features and pricing for details.
Summary
- INP replaced FID in March 2024 and is the Core Web Vital Google currently ranks on. It measures the worst interaction latency across a visit, not the first one.
- The thresholds are 200ms (Good) and 500ms (Poor), measured as the p75 of CrUX field data over a rolling 28-day window per URL group.
- INP is fundamentally a field metric. Lab tools (Lighthouse, WebPageTest) can approximate it but the number Google uses comes from real users.
- Instrument with
web-vitalslibrary, capture attribution (CSS selector + load state + device + release), beacon to an async endpoint. - Cut your data per route, per device class, per connection, per release. Aggregate INP across all of these together is rarely actionable.
- The common INP killers: autocomplete handlers, heavy onClick in lists, virtualised-list re-renders, tag managers, framework reactivity cascades, page-load hydration, long tasks.
- The 28-day CrUX window means your fix takes weeks to show in Search Console. Trust your own RUM, not the Search Console card, for fix-confirmation.
- Bucket INP by release version. Release cohort comparisons catch regressions far faster than aggregate trends.
- Treat the beacon endpoint as critical infrastructure: external monitoring on it, sampling strategy, async write path.
INP is the metric where front-end performance discipline most directly translates to SEO ranking. Get the instrumentation right once, build the per-route dashboard, and the rest of the work — finding bad handlers, splitting work across frames, deferring tag scripts — happens in the right places instead of via guesswork.