TL;DR
On March 12,2024, Google retired First Input Delay and made Interaction to Next Paint (INP) a Core Web Vital. It is a much harder test: it measures the full latency of every tap and click across the whole visit, not just the delay before the first one. A good INP is 200ms or less at the 75th percentile; anything over 500ms is poor. INP is now the single most commonly failed Core Web Vital, roughly 43% of sites still miss the 200ms bar, and the cause is almost always your own JavaScript hogging the main thread. That sluggish "I tapped and nothing happened" feeling is a ranking signal and a conversion leak at the same time, and unlike LCP it gets worse the more interactive your app is.
The metric that quietly got three times harder
For years, teams gamed responsiveness. First Input Delay only measured the gap between a user's first interaction and the moment the browser began to process it. It said nothing about whether the page actually did anything useful afterward, and it ignored every interaction after the first. A page could score a flawless FID and still feel like wading through wet sand on the third, fourth, and fortieth tap. The metric was so easy to pass that the vast majority of sites scored "good" on it, which is precisely why Google replaced it. A metric that everyone passes is not measuring anything.
INP closed that loophole. Per Google's own web.dev documentation, INP observes all the interactions a user makes on a page and reports a value near the worst one. Crucially, it measures the full interaction lifecycle: input delay (waiting for the main thread to be free), processing time (your event handlers running), and presentation delay (the browser rendering the next frame). FID only ever saw that first slice, and only the delay portion of it. INP sees the whole thing, end to end, repeatedly, which is why a site that passed FID comfortably can fail INP badly the day the switch flips.
Almost half the web is failing this one
INP is not a niche problem. According to CrUX field data summarized across the web, around 43% of sites fail the 200ms INP threshold, making it the most commonly failed of the three Core Web Vitals, worse than LCP and far worse than CLS. The reason is structural: the modern web ships enormous amounts of JavaScript, and JavaScript runs on a single main thread that also has to handle every user interaction.
When that thread is busy parsing, hydrating, executing analytics, or running a framework's reconciliation pass, it cannot respond to a tap. The tap queues. The user waits. INP records the wait. The heavier your bundle and the more third-party tags you load, the worse it gets, and the gap is widest on the mid-range Android phones that make up the majority of real traffic, not the flagship device on your desk. There is a cruel irony here: the more you invest in rich, interactive, JavaScript-heavy experiences, the more interaction surface you create and the more main-thread work you pile on, so your INP risk rises in direct proportion to how "modern" your front end is.
The business consequence is twofold. INP is a confirmed ranking signal in Google's page-experience systems, so a poor score is a quiet drag on organic visibility. And separately, a laggy interface is a direct conversion problem: the user who taps "add to cart" and sees nothing happen taps again, gets two items, or gives up. Responsiveness failures concentrate in exactly the high-intent moments, checkout, filtering, search, where hesitation costs the most.
Where the milliseconds actually go
To fix INP you have to know which of the three phases is killing you. Most teams assume it is processing time, their own slow handlers, when the real culprit is usually input delay: the main thread was already blocked by something unrelated when the user tapped. Guessing wrong here wastes weeks optimizing the part that was never the bottleneck.
Long tasks are the main villain
A "long task" is any chunk of main-thread work over 50ms. While a long task runs, the browser is deaf to input. Stack a few of these during page load or after a route change, framework hydration, a 200KB analytics script, an A/B testing snippet rewriting the DOM, and any interaction in that window inherits the full delay. The DebugBear INP guide notes that breaking up long tasks and yielding to the main thread is the highest-leverage fix for most sites. A single 400ms hydration task is enough, on its own, to blow your INP for any user who interacts during it.
Heavy event handlers
The second offender is your own code: a click handler that synchronously filters 5,000 rows, recalculates layout, and re-renders a component tree before the browser paints. Everything between the click and the next paint counts. If you do expensive work in the handler, you pay for it in INP. A common pattern is the over-eager state update that triggers a full re-render of a large component tree on every keystroke in a search box, each keypress is an interaction, and each one drags.
Layout thrashing and large DOMs
Even after your handler runs, the browser still has to render the next frame. A DOM with tens of thousands of nodes, or a handler that reads and writes layout properties in a loop (forcing synchronous reflows), inflates presentation delay. Big DOM, big paint, bad INP. This is why infinite-scroll pages and sprawling data grids are chronic INP offenders: the DOM grows without bound, and every interaction has to repaint more of it.
Measure it like a user, not like a demo
INP is a field metric. Lab tools can simulate it, but the score Google ranks on comes from real Chrome users in the CrUX dataset. That means two things: you must measure in production with the web-vitals library, and you must attribute each bad interaction to the element and the script that caused it. Aggregate scores tell you that you have a problem; attribution tells you which button on which template to fix.
import {onINP} from 'web-vitals/attribution';
onINP((metric) => {
const a = metric.attribution;
// Send the WORST interaction with full attribution to your RUM endpoint
navigator.sendBeacon('/rum/inp', JSON.stringify({
value: Math.round(metric.value), // total INP in ms
rating: metric.rating, // good | needs-improvement | poor
target: a.interactionTarget, // CSS selector of the element
type: a.interactionType, // pointer | keyboard
inputDelay: Math.round(a.inputDelay), // thread was busy this long
processing: Math.round(a.processingDuration), // your handlers ran this long
presentation: Math.round(a.presentationDelay), // render time
longTask: a.longAnimationFrames?.length || 0,
url: location.pathname,
}), {type: 'application/json'});
});
The breakdown is the whole game. If inputDelay dominates, your problem is something else hogging the thread, defer it. If processingDuration dominates, your handler is too heavy, split it. If presentationDelay dominates, your DOM or CSS is the bottleneck. Pivot the data by URL and element selector and the worst offenders fall out immediately: usually one or two interactions on one or two templates account for most of your bad INP.
The fixes, in order of leverage
1. Break up long tasks with yielding
The single most effective change is to stop monopolizing the main thread. Yield back to the browser between chunks of work so it can service pending interactions. The modern primitive is scheduler.yield(); a setTimeout(0) fallback works everywhere. Yielding does not make the total work shorter, but it creates gaps in which the browser can respond to a tap, which is exactly what INP measures.
async function processInChunks(items, work) {
for (let i = 0; i < items.length; i++) {
work(items[i]);
// Every 50 items, hand the thread back so taps can be serviced
if (i % 50 === 0) {
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise((r) => setTimeout(r, 0));
}
}
}
}
2. Defer non-critical work past the interaction
Move anything that does not need to happen before the next paint to after it. Update the visible UI first, then run analytics, logging, and persistence in a requestIdleCallback or after a yield. The user sees the result immediately; the bookkeeping happens when the thread is idle. A click handler should do the minimum to reflect the user's action visually, and nothing more, on the critical path.
button.addEventListener('click', async () => {
// 1. Visible feedback FIRST, cheap, immediate, paints fast
button.classList.add('is-active');
showOptimisticState();
// 2. Let the browser paint the response before heavy work
await scheduler.yield?.() ?? new Promise(r => requestAnimationFrame(r));
// 3. Now do the expensive stuff, INP is already recorded as fast
await persist();
trackAnalytics('click');
});
3. Cut and isolate third-party JavaScript
Tag managers, chat widgets, and A/B tools are frequent INP killers because they run on your main thread on a schedule you do not control. Audit them, remove what you can, and move what you cannot to a web worker with a tool like Partytown. Every script you delete is input delay you stop paying for, and because these scripts often run on timers and triggers, they frequently collide with the exact moment a user is trying to interact.
4. Shrink and virtualize the DOM
Render only what is visible. Virtualized lists, code-split routes, and lazy-mounted components keep the DOM small, which directly lowers presentation delay on every interaction. For data grids and feeds, windowing is not a nice-to-have; it is the difference between a 100ms paint and a 600ms one.
Gate it in CI before it ships
INP regressions arrive quietly. A new dependency, a heavier hydration path, or one more marketing tag can push you over 200ms with no visible symptom in code review. The fix is to assert an INP budget against a lab measurement on a throttled profile in your pipeline, so a responsiveness regression fails the build instead of the field data weeks later.
# CI gate: fail the build if simulated INP regresses past budget
const INP_BUDGET_MS = 200;
const trace = await runUserFlow(page, [
() => page.click('#add-to-cart'),
() => page.click('#open-filters'),
() => page.type('#search', 'wireless headphones'),
]);
const worstINP = Math.max(...trace.interactions.map((i) => i.duration));
if (worstINP > INP_BUDGET_MS) {
console.error(`INP ${worstINP}ms exceeds ${INP_BUDGET_MS}ms budget`);
process.exit(1);
}
Pair that lab gate with continuous field monitoring. The lab catches regressions before release; the field tells you what the 75th-percentile user actually experiences. You need both, because the lab cannot reproduce the messy reality of a real device juggling background tabs, a slow network, and three extension scripts, and the field cannot catch a regression before it has already shipped to users. A scan that exercises your real interaction flows under throttled mobile conditions is what bridges the two: it reproduces the worst-case interaction before release and attributes it to a fixable element.
The bottom line
INP replaced FID because FID was too easy to pass while still shipping a laggy page. Now the bar is real, the measurement covers every interaction across the whole visit, and nearly half the web is failing it. The cause is almost always main-thread contention from your own and third-party JavaScript, and the fixes, yielding, deferring, trimming third parties, shrinking the DOM, are well understood. What is missing on most teams is the data: field INP with per-element attribution that points at the exact button and script costing you the most. Scan your real interactions under mobile conditions, fix the worst-attributed handlers first, gate the budget in CI, and monitor the field so the responsiveness you win does not quietly erode the next time someone adds a tag.
Find the Gaps Before They Cost You
Scan audits your site for the accessibility, performance, AEO, and security gaps that quietly drain revenue and invite lawsuits, in one pass.
Try Scan Free →