Interaction to Next Paint turns two as a Core Web Vital. Most JS-heavy sites still fail it, and no checklist fixes that.
INP punishes main-thread JavaScript wherever it runs — your code, your build, or a script you don’t own.
What Interaction to Next Paint actually measures
On March 12, 2024, Google made Interaction to Next Paint the third Core Web Vital, retiring First Input Delay after roughly two years of the two metrics running side by side. FID measured one thing: how long the browser waited before it started processing a user’s first click, tap, or keypress on a page. Everything that happened after that moment, the event handler executing, the DOM updating, the browser painting the result, sat outside the metric entirely.
A page could score perfectly on FID while stuttering on every interaction after the first one, and plenty did. INP closes that gap. It samples interactions across a page’s full session, not just the first, and reports something close to the worst of them (the 98th percentile on pages with several interactions, or the single slowest on pages with only a few). A "good" score is under 200 milliseconds, measured from input to the frame that shows the result on screen. That is a stricter number, and it is why a metric with a two-year runway to prepare for it still trips up products that were never architected with it in mind.
The 50-millisecond line that decides whether an interaction feels broken
The Long Tasks API classifies any script execution longer than 50 milliseconds on the main thread as a "long task." That threshold matters for INP because the main thread is single-threaded for JavaScript execution, style recalculation, and layout. While a long task is running, the browser cannot start processing a click or keypress that arrives mid-task. The interaction just queues behind whatever is already running, and the user feels the wait as unresponsiveness, not as an error.
This is why INP failures rarely show up as one obvious freeze. They show up as background work, a search-as-you-type debounce, a chart re-render, an analytics beacon, quietly running past 50 milliseconds and eating into the response budget for whatever the user does next.
Three causes, and none of them share a fix
Field data and profiling traces on JavaScript-heavy products keep pointing at the same three sources of poor INP. They do not respond to the same intervention, which is the main reason a generic "fix your INP" checklist rarely survives contact with a real codebase.
| Cause | Where it shows up | What actually fixes it |
|---|---|---|
| Heavy event handlers | Search-as-you-type, client-side filtering, form validation, large DOM mutations tied directly to a click or keystroke | Break the handler into chunks with scheduler.yield() or defer non-critical work |
| Third-party scripts | Chat widgets, analytics tags, ad scripts, and consent managers running work during or right after an interaction | A script budget with an accountable owner, not a code change |
| Layout thrashing | Alternating reads (offsetHeight, getBoundingClientRect) and writes to the DOM inside the same handler | Batch reads before writes; replace layout polling with ResizeObserver |
Fixing heavy event handlers is an engineering task. Fixing third-party scripts is a procurement and governance task, and the team that can fix it usually is not the team that gets asked to. Layout thrashing is close to invisible in a flame graph unless you already know to look for the alternating read-write-read-write pattern, which is why it is the cause most often missed in a first pass.
What scheduler.yield() actually buys you
scheduler.yield() is part of the Prioritized Task Scheduling API. It lets a long-running function hand control back to the browser between chunks of work, so a pending click or keypress gets a chance to run before the function continues. A loop that used to block the thread for 300 milliseconds can yield every few iterations and let the browser interleave the user’s interaction in between.
async function processRows(rows) {
for (let i = 0; i < rows.length; i++) {
renderRow(rows[i]);
// Hand control back to the browser every few rows so a
// pending interaction (click, keypress) can run before we continue.
if (i % 20 === 0) {
await scheduler.yield();
}
}
}scheduler.yield() only addresses the first cause in the table above: heavy work in code you control. It does nothing for a third-party script blocking the thread, and it does nothing for layout thrashing, since neither of those is a long-running function you can insert yield points into.
Third-party scripts are a budget problem, not a code problem
Engineering can wrap first-party code in yield points and still fail INP if a chat widget, a consent banner, or an ad tag blocks the thread during an interaction. That script was not written by the team getting asked to fix the metric, and it usually cannot be rewritten at all. The available levers are different: load it after the interaction that matters most, sandbox it in an iframe or a worker where the vendor supports that, or cap total third-party JavaScript weight as a condition of adding a new tag in the first place.
None of that is a sprint ticket. It is a standing budget, enforced at the point a new script gets added, not discovered after the metric has already slipped.
A triage method that does not start with the homepage
Because INP is measured per interaction and reported near the worst case, profiling the homepage on a synthetic run often points at the wrong problem. A more reliable triage sequence:
- Pull INP attribution from field data (the CrUX API, PageSpeed Insights, or your own real-user monitoring) broken down by interaction target, not just by page.
- Rank interactions by frequency multiplied by current INP, not by raw INP alone. A rare, slow interaction usually matters less than a common, mediocre one.
- For each of the worst offenders, classify it against the three causes above before writing any code.
- Fix it, then re-measure with field data, not just a DevTools performance trace. Synthetic runs do not reproduce the device and network variance real users hit.
What changes on React Server Components or signals-based frameworks
Frameworks built around server rendering and fine-grained reactivity reduce how much client-side JavaScript has to run per interaction, which helps the first cause in the table by construction: there is simply less code that can turn into a long task. That does not touch the second or third cause. A page built entirely on server components can still fail INP if a third-party tag or a naive resize handler is doing the damage.
“A smaller JavaScript bundle changes how much work an interaction can afford. It does not decide whether that budget gets spent.”
The metric is not going away, and the gap between what a checklist can fix and what an interaction actually costs is not closing on its own. Teams that treat INP as a shared budget between engineering, third-party vendors, and whoever owns the consent banner keep passing it. Teams that treat it as one sprint’s worth of Lighthouse fixes pass it once, and fail it again the next time someone adds a script tag.
Frequently asked questions
Related reading
AWS had four separate outages in 2026. Multi-region would have caught exactly one.
Four AWS outages hit in eleven weeks in 2026: a data center overheating, a third-party network failure, a software limit, and a hardware routing fault. The standard fix, multi-region, would have caught exactly one.
Observability bills don't explode from traffic. Metric cardinality does, and LLM telemetry is the fastest way to trip it
Teams blame traffic when an observability bill triples after shipping an LLM feature. The real driver is metric cardinality, and moving to OpenTelemetry doesn't fix it if the same tagging habits come along.
The AI memory shortage just rewrote the cloud cost-optimisation playbook
DRAM and NAND contract prices rose roughly 95% in a single quarter. The cause is a global reallocation of memory manufacturing towards AI accelerators, and the usual cost-optimisation playbook does not touch it.