Cut Your INP Below 200ms Before It Tanks Rankings
A field-data playbook for diagnosing and fixing Interaction to Next Paint, the metric that replaced FID and now decides your Core Web Vitals pass.
On March 12, 2024, Google quietly changed the rules of the responsiveness game. Interaction to Next Paint replaced First Input Delay as a Core Web Vital, and a lot of sites that had been comfortably green on FID woke up amber or red overnight. The reason is simple and unforgiving: FID only ever measured the delay before the browser started handling your first interaction. INP measures the full round trip, for every interaction, and reports the worst one.
To fix INP, get it under 200 milliseconds at the 75th percentile of your real visitors by breaking each interaction into input delay, processing time, and presentation delay, then cutting whichever one is fattest: yield long tasks off the main thread, trim heavy event handlers, and reduce layout and paint cost.
That shift exposes the work your page actually does when someone clicks, types, or taps. The dropdown that takes a beat to open. The filter that locks the page for 300 milliseconds while React re-renders the whole list. None of that ever showed up in FID. All of it shows up in INP, and your search ranking now depends on getting it under control. It is not an academic ranking signal either, since every slow second has a measurable cost in revenue.
What the number is actually measuring
A "good" INP is 200 milliseconds or less at the 75th percentile of your real visitors. Between 200 and 500 milliseconds needs improvement, and anything above 500 milliseconds is poor. The 75th percentile matters: it means at least three out of four interactions on the page have to finish in under 200ms, so a single janky modal that fires often will sink the whole score.
Every interaction INP measures breaks into three parts, and you fix INP by attacking whichever part is fat:
- Input delay: the time from the user's action to your event handler starting to run. This is the main thread being busy with something else, usually a long task left over from page load or a third-party script.
- Processing time: how long your event handlers run. This is your code. The click listener that synchronously recalculates a cart, the keystroke handler that re-renders a 500-row table.
- Presentation delay: the time from your handlers finishing to the browser painting the next frame. Heavy layout, large DOM, expensive style recalculation.
You cannot fix what you have not attributed. Field data first.
Get field data before you touch anything
Lab tools like Lighthouse cannot measure INP properly, because INP needs real interactions. Pull your field data instead. The Chrome User Experience Report (CrUX) gives you the 75th-percentile INP that Google actually scores you on, and the PageSpeed Insights API surfaces it per URL.
For the granular attribution that tells you which element and which phase is the culprit, instrument the page with the web-vitals library's attribution build. It hands you the offending element, the interaction type, and the breakdown across input delay, processing, and presentation. Send that to your analytics endpoint and you stop guessing.
import { onINP } from 'web-vitals/attribution';
onINP((metric) => {
const a = metric.attribution;
navigator.sendBeacon('/vitals', JSON.stringify({
value: metric.value,
target: a.interactionTarget,
type: a.interactionType,
inputDelay: a.inputDelay,
processingDuration: a.processingDuration,
presentationDelay: a.presentationDelay,
}));
});
A week of that data tells you whether your problem is a busy main thread, your own handler code, or rendering, and you fix the right thing instead of the convenient thing.
Break up the long tasks stealing your input delay
Long tasks are any chunk of main-thread work over 50 milliseconds. While one runs, the browser cannot start handling a click, so the click waits. That wait is your input delay, and it is the most common reason a fast handler still scores badly.
The old fix was setTimeout(fn, 0) to chop work into pieces, but that pushes your continuation to the back of the task queue behind any other pending work. The modern tool is scheduler.yield(), available in Chrome and built to preserve your task's priority. You yield to the main thread, the browser handles any pending user input first, then resumes your code as soon as it can. The full playbook for slicing up these blocks lives in breaking up long tasks so your main thread stays responsive.
async function processChunks(items) {
for (const item of items) {
doWork(item);
if (navigator.scheduling?.isInputPending?.()) {
await scheduler.yield();
}
}
}
The pattern is "do a little, check for pending input, yield, repeat." A 400-millisecond hydration loop becomes a series of 8-millisecond slices with the user's clicks slipping in between them. For browsers without scheduler.yield, fall back to a scheduler.postTask or a setTimeout-based yield, but the principle holds: never let a single synchronous task hog the thread while the user is trying to interact.
Shrink the processing time in your handlers
Once input delay is under control, look at what your handlers actually do. Three patterns cover most of the damage.
Separate the visual feedback from the heavy work. When someone clicks, the only thing that has to happen before the next paint is the feedback: the button presses in, the spinner appears, the input shows the typed character. Do that, yield, then do the expensive part. The user perceives an instant response even though the real work takes another 150ms in the background.
Stop re-rendering everything. In React, an unmemoized list that re-renders on every keystroke is a classic INP killer, and it is often the same accumulating render cost behind a SPA that gets slower by the hour. Debounce the expensive derived state, memoize the rows, and consider a transition (useTransition / startTransition) so urgent UI updates are not blocked behind a large non-urgent re-render. The transition tells React the big update is interruptible, which is exactly what INP rewards.
Defer non-critical JavaScript. Every third-party tag, analytics beacon, and chat widget competes for the same main thread, and the same render-blocking resources that steal your first paint tend to be the ones still hogging the thread when the first click arrives. Audit them, load what you can with async/defer, and push the truly non-essential ones behind requestIdleCallback or a delayed init after first interaction. Each script you remove from the critical window is input delay you stop paying, which is why taming the third-party scripts wrecking your page speed is so often the fastest INP win. Shipping less JavaScript in the first place helps too, and server components that ship zero JS cut the bundle the main thread has to chew through.
This is the same discipline we apply when we build full-stack web apps that have to stay responsive under real load, where a sluggish filter or a janky table is not a polish issue but a conversion and ranking issue.
Cut presentation delay with a leaner DOM
Presentation delay is what is left when your handlers finish but the browser still has to recalculate styles, run layout, and paint. The biggest lever is DOM size. A page with 5,000 nodes recalculates style across all of them on a single class change, and that recalculation lands inside your INP window.
Tactics that move the number:
- Virtualize long lists. Render the rows in view, not the 2,000 in the dataset. The DOM stays small and every interaction paints fast.
- Use
content-visibility: autoon offscreen sections so the browser skips layout and paint for content the user has not scrolled to yet. - Avoid layout thrash. Batch DOM reads and writes. Reading
offsetHeightin a loop after each write forces synchronous reflow over and over, and that reflow is presentation delay you are inflicting on yourself. - Pull animations off the main thread. Animate
transformandopacity, which the compositor handles, instead oftop,left,width, orheight, which force layout on every frame. The same render-cost discipline keeps cumulative layout shift at zero on dynamic pages, the other half of a clean Core Web Vitals score.
Verify against the field, not the lab
Here is the trap that wastes weeks: you fix something, Lighthouse looks great, and your CrUX INP does not move. Lab tools simulate; they do not click. This is the same lesson behind why your Lighthouse score lies and field data tells the truth. The only verdict that counts is field data, and CrUX updates on a rolling 28-day window, so a real fix takes weeks to fully show up in the score you are graded on.
Run a tighter loop locally. Open Chrome DevTools, record a Performance trace, and actually interact with the slow element. The trace marks the interaction and shows you the input delay, the script that ran, and the paint. Get that single interaction under 200ms in the trace, ship it, then watch CrUX confirm it across your real traffic over the following weeks.
The teams that win at INP treat it as an ongoing field-data practice, not a one-time audit. They instrument with attribution, watch the 75th percentile, and chase whichever phase is fat this month. That discipline is also the difference between a Core Web Vitals score that quietly slips after the next feature ships and one that holds, which is why the teams who keep it green usually back it with a performance budget the whole team agrees not to break. If your interactions feel snappy in development but your real-user INP tells a different story, the gap is almost always one busy main thread away, and that gap is findable. Start with the field data, attribute the phase, and fix the part that is actually slow.






