Interaction to Next Paint (INP) is poor: find the slow interaction
INP is Good at 200 ms or less and poor above 500 ms. Find the slow interaction with the web-vitals attribution build, then break up its long task.
LLaunchScaler·Published ·8 min read
Interaction to Next Paint is poor when the slowest interactions on your page take more than 500 ms from the click, tap or keypress to the next frame; it is Good at 200 ms or less at the 75th percentile. To fix it, find the specific interaction that is slow with the web-vitals library's attribution build, see whether the time goes to input delay, processing or presentation, then break up the long task behind it with scheduler.yield() or setTimeout and defer work that does not need to happen before the next paint.
INP replaced First Input Delay as a Core Web Vital on March 12, 2024. Unlike FID, which only timed the delay before the first interaction's handlers started, INP covers the whole interaction and all interactions on the page. That is why pages that passed FID easily can fail INP.
What is INP and what counts as poor?
INP "assesses a page's overall responsiveness to user interactions by observing the latency of all click, tap, and keyboard interactions" during a visit, according to web.dev. The value reported is the longest interaction, "ignoring outliers": web.dev ignores "one highest interaction for every 50 interactions." The 75th percentile across page views is then compared with the thresholds.
INP at the 75th percentile
Rating
200 ms or less
Good
Over 200 ms up to 500 ms
Needs improvement
Over 500 ms
Poor
Scrolling and hovering do not count, so a page where visitors only scroll can have no INP data at all. When INP data is missing, PageSpeed Insights assesses Core Web Vitals on LCP and CLS alone. If INP is what failed your assessment, explains how the verdict is built.
Questions, answered
What people ask about this
01
What is a good INP score?
200 milliseconds or less at the 75th percentile of page views is Good. Over 200 ms up to 500 ms needs improvement, and above 500 ms is poor. INP replaced First Input Delay as a Core Web Vital on March 12, 2024.
The main thread does one thing at a time. web.dev defines "any task that takes longer than 50 milliseconds" as a long task. While a long task runs, a click cannot be handled, and after the handler runs, the browser cannot paint until the thread is free again. Poor INP is usually long tasks landing at the moment someone interacts.
web.dev splits every interaction into three parts, and the web-vitals library reports each one:
Part
What it covers
Typical cause when it is long
Fix
Input delay
From the interaction until event handlers start
Other work already on the main thread: script evaluation during load, hydration, third-party tags, timers
Break up and defer that other work
Processing duration
The event handlers themselves
Too much work in click, input or keydown handlers
Do only the visual update first; yield before the rest
Presentation delay
From handlers finishing until the next frame is shown
Large DOM updates, layout thrashing, rendering big chunks of HTML with JavaScript
Update less of the DOM; avoid reading layout right after writing styles
The usual suspects in practice:
Heavy JavaScript runs at load. web.dev notes that script evaluation during startup "can introduce long tasks on the main thread, which will delay the browser from responding to other user interactions." In React frameworks, hydration is part of that startup work.
Third-party scripts such as tag managers, chat widgets, analytics and A/B testing tools, which run on the same main thread as your code.
Event handlers that do everything at once: update the UI, recalculate derived data, write to storage and send analytics in one synchronous block.
Large client-side renders, where one interaction re-renders a long list or a whole page.
How do you find the slow interaction?
Collect INP from real users with the attribution build of the web-vitals library, which tells you which element was involved and where the time went. CrUX tells you whether INP is poor but, as web.dev puts it, "it can't tell you what caused the problem." The attribution build is about 1.5 KB larger than the standard one.
Change the import to the attribution build and log or send what it reports:
import { onINP } from "web-vitals/attribution";
onINP(({ value, rating, attribution }) => {
const {
interactionTarget, // CSS selector of the element the user interacted with
interactionType, // "pointer" or "keyboard"
loadState, // document state when it happened, e.g. "dom-interactive"
inputDelay,
processingDuration,
presentationDelay,
longestScript, // summary of the longest script in that frame, where supported
} = attribution;
navigator.sendBeacon("/rum", JSON.stringify({
value, rating, interactionTarget, interactionType, loadState,
inputDelay, processingDuration, presentationDelay,
script: longestScript?.entry?.sourceURL,
}));
});
Then read your data like this:
Group reports by interactionTarget and sort by count of poor values. The top selector is the interaction to fix first.
Look at which of the three parts is largest for that target. That tells you whether to fix the handler (processing), other work around it (input delay) or the rendering (presentation).
Check loadState. The web-vitals README notes that interactions "while the document was loading and executing script" can "result in long delays," which points to load-time JavaScript rather than the handler.
Where the browser supports the Long Animation Frame API, longestScript names the script responsible, which exposes third-party code quickly.
To reproduce it in the lab, open Chrome DevTools, go to the Performance panel, record, and perform that interaction. web.dev also suggests "interacting with the page as it loads," when the main thread is busiest, because testing only after the page is idle can miss the problem.
Why doesn't Lighthouse show INP?
A standard Lighthouse run loads the page and never clicks anything, so there is no interaction to measure. web.dev says "some lab tools won't report a page's INP because they only observe the loading of a page without any interactions," and that Total Blocking Time "may be a reasonable proxy metric for INP, but it's not a substitute for INP."
That explains a common pattern in PageSpeed Insights: the field INP is poor while the lab section looks acceptable. Use TBT and the main-thread diagnostics as hints about load-time long tasks, and use field data with attribution to find the real slow interaction.
How do you break up long tasks?
Yield to the main thread inside long work so the browser can handle input and paint between chunks. web.dev recommends scheduler.yield(), which "returns a Promise that will be resolved in a future task," with the advantage that "its continuation is prioritized." It is not yet supported in all browsers, so wrap it with a setTimeout fallback.
The fallback web.dev gives:
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
// Fall back to yielding with setTimeout.
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
async function saveSettings(items) {
for (const item of items) {
process(item);
await yieldToMain(); // give the browser a chance to handle input and paint
}
}
setTimeout on its own also works but has two drawbacks web.dev names: the deferred work goes to the back of the task queue, and "after five rounds of nested setTimeout()s, the browser will start imposing a minimum 5 millisecond delay" for each one.
How do you make event handlers faster?
Do only the work needed for the next frame inside the handler, and defer the rest. web.dev's example is a text editor where typing must update the text box immediately, while the word count, spell check and save can wait. Its pattern defers the non-urgent work until after the next frame:
textBox.addEventListener("input", (event) => {
updateTextBox(event); // visible change, needed for the next frame
requestAnimationFrame(() => {
setTimeout(() => {
const text = textBox.textContent;
updateWordCount(text);
checkSpelling(text);
saveChanges(text);
}, 0);
});
});
Apply the same split to common SaaS interactions:
A "Save" button: show the saving state first, then serialise, validate and send in a later task.
A filter or search box: update the input and a loading state immediately, then filter the list after yielding, or debounce it.
A menu or modal: open it first, then load analytics, prefetch data or render heavy content inside it afterwards.
Any handler that writes styles and then reads layout values (like offsetHeight): batch the reads before the writes to avoid layout thrashing, which web.dev lists as a presentation-delay cause.
How do you reduce load-time and third-party work?
Ship less JavaScript and run it later. Input delay during load comes from script evaluation, so the less code that runs before the page is usable, the shorter the delay. Code-split by route, load interaction-only components when they are needed, and remove dependencies you no longer use; reducing unused JavaScript covers how to find them.
For third-party scripts, load them with async or defer, start chat widgets and embeds on first interaction rather than on load, and remove tags nobody uses. For React and Next.js apps, keep the client component tree small so there is less to hydrate, and fix hydration mismatches, which make React discard server HTML and re-render on the client; hydration errors in Next.js covers the common ones.
How do you confirm an INP fix?
Check the interaction in the lab first, then wait for the field data. INP in CrUX covers a trailing 28-day window, so the public number moves slowly; your own attribution data shows the change within days.
In Chrome DevTools, open the Performance panel, turn on CPU throttling to approximate a mid-range phone, and record the interaction you fixed.
Look at the main-thread track for the interaction. No task around it should run longer than 50 ms, and the frame should paint soon after the input.
Repeat the interaction while the page is still loading, since that is when input delay is worst.
Deploy, then watch the 75th percentile of INP for that interactionTarget in your field data.
Check PageSpeed Insights again after the 28-day window has mostly turned over.
Check INP and its causes on your page
To see your page's responsiveness from real users and the load-time causes behind it, run the free scan on LaunchScaler with the URL, no account needed. Its speed checks read INP from Chrome field data at the 75th percentile, measure Total Blocking Time as the lab stand-in, and flag the usual causes: main-thread work during load, third-party script cost and unused JavaScript. Its does-it-work checks also catch hydration mismatches, which add client-side work of their own.
Long tasks on the main thread. Any task over 50 milliseconds blocks input, so a click waits behind script evaluation during load, heavy event handlers, third-party scripts or expensive rendering. The fix is to find the slow interaction and break up the work behind it.
03
How do I find which interaction is slow?
Use the attribution build of the web-vitals library. Its onINP callback reports interactionTarget (a selector for the element), the interaction type, the load state, and the time split into input delay, processing duration and presentation delay, so you know what to fix.
04
Why doesn't Lighthouse show INP?
A standard Lighthouse run only loads the page and never interacts with it, and INP needs a real click, tap or keypress. Lighthouse reports Total Blocking Time instead, which web.dev calls a reasonable proxy for INP but not a substitute.
05
What is scheduler.yield()?
A browser API that returns a promise resolved in a new task, letting a long function pause and give the main thread back. Its continuation is prioritized over other queued tasks. It is not supported in every browser yet, so use a setTimeout fallback.
Split LCP into four sub-parts, compare each to its budget and fix the slow one: server time, a late or lazy hero image, a heavy file or blocked rendering.
Run the URL through LinkedIn's Post Inspector, then fix what it shows: missing og tags, an image under 1200 x 627 px, a blocked LinkedInBot or a redirect.
New Lovable apps render on the server; older ones are pre-rendered for verified crawlers. Find your stack, then fix domain, sitemap, robots and titles.