How to improve INP: what delays the response to a click
INP adds up input delay, processing and presentation. How to find the slow interaction and cut each phase, with scheduler.yield and LoAF.
On this page 8 min read
INP (Interaction to Next Paint) measures how long the page takes to show a visual response after a click, tap or key press. It observes the interactions across the whole visit and reports the slowest one (on pages with many interactions, it ignores the highest one for every 50). It is good up to 200 ms and poor above 500 ms, at the 75th percentile of visits (web.dev). To reduce it, find out which interaction is slow, see which of the three phases holds the time (waiting, running the code or painting) and fix that phase.
This article follows the web.dev guides on optimizing INP (updated in September 2025) and on long tasks. For the context of the three Core Web Vitals, see the Core Web Vitals guide.
What INP measures
| Range | INP at the 75th percentile |
|---|---|
| Good | up to 200 ms |
| Needs improvement | above 200 ms and up to 500 ms |
| Poor | above 500 ms |
Mobile and desktop are assessed separately. The interaction starts with the user's gesture and ends when the browser paints the next frame on screen. This means INP does not measure when your code finishes, but when the user sees something change. When the page comes back from the back/forward cache (bfcache), INP starts again from zero, because to the user it is a new visit (web.dev).
The three phases of an interaction
| Phase | What it is | Common cause |
|---|---|---|
| Input delay | From the gesture until the first event handler starts running. | The main thread was busy with something else: scripts loading, timers, another interaction. |
| Processing duration | The time the event handlers take to run. | Too much code inside the click or input handler. |
| Presentation delay | From the end of the handlers until the frame appears on screen. | Expensive style recalculation, layout and paint, a large DOM, a lot of HTML generated by JavaScript. |
The three phases are measured by adding up all the events in the same frame (for example pointerup, mouseup and click from a single click), and that is how the web-vitals library calculates them (web.dev).
Long tasks: where most problems start
The browser runs almost everything on a single main thread: parsing HTML and CSS, running JavaScript, calculating layout and painting. Each block of work is a task. A task over 50 ms is called a long task (web.dev). While it runs, a click waits in the queue. If the task is running when the user clicks, input delay goes up. If the task is the click handler itself, processing duration goes up. The remedy is the same in both cases: break the work into smaller pieces and give control back to the browser between them.
How to find the slow interaction
Knowing that INP is high does not tell you which button is the problem. The path web.dev recommends goes from the field to the lab (web.dev):
Check CrUX in PageSpeed Insights or Search Console. It tells you whether there is an INP problem on mobile and desktop, but not which interaction causes it.
Collect real-user data with the attribution build of the web-vitals library. It reports the element that was clicked, the interaction type and the time of each phase.
Use Long Animation Frames (LoAF) data to get to the responsible script.
Reproduce the interaction in Chrome DevTools to investigate and test the fix.
With the web-vitals library
import {onINP} from 'web-vitals/attribution';
onINP(({value, attribution}) => {
console.log('INP', value);
console.log('Element', attribution.interactionTarget);
console.log('Type', attribution.interactionType); // 'pointer' or 'keyboard'
console.log('Input delay', attribution.inputDelay);
console.log('Processing', attribution.processingDuration);
console.log('Presentation', attribution.presentationDelay);
console.log('Page state', attribution.loadState);
console.log('Longest script', attribution.longestScript);
});The loadState field shows whether the interaction happened while the page was still loading, when input delay tends to be higher. longestScript gives the longest script that overlapped with the interaction and the phase it ran in. These LoAF-based fields are only filled in on browsers that support the API (GitHub GoogleChrome/web-vitals).
With the Long Animation Frames API
A long animation frame is a frame whose visual update was delayed by more than 50 ms. Each entry lists the scripts over 5 ms that ran in the frame, with the file URL, the function name, what invoked the script (an event listener, a requestAnimationFrame, a timer) and how much time went into forced layout (Chrome for Developers). The script URL lets you tell your own code apart from third-party code.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.duration, entry.blockingDuration, entry.scripts);
}
});
observer.observe({type: 'long-animation-frame', buffered: true});LoAF has been available in Chrome and Edge since version 123. Firefox and Safari do not support it (MDN browser-compat-data). Scripts from cross-origin iframes, workers and extensions do not show up in the attribution.
With Chrome DevTools
The Performance panel has a live metrics view. As you interact with the page, it records the local INP and lists each interaction with the element, the time and the three phases (Chrome for Developers). To investigate a specific interaction:
Turn on CPU throttling, or use a low-end or mid-range Android phone with remote debugging. Fast machines hide long tasks.
Click record, perform the interaction and stop the recording.
In the Interactions track, the interaction appears above the main thread activity. The left whisker is the input delay, the solid block is processing and the right whisker is presentation.
Also test during loading, right after the page appears, when the main thread is usually busier.
This walkthrough is the one web.dev gives for manual diagnosis in the lab.
How to reduce input delay
Input delay is time during which your handler has not even started. The cause is something else occupying the main thread at the moment of the click:
Script evaluation during loading. Parsing, compiling and executing JavaScript creates long tasks right when users tend to interact for the first time. Less JavaScript in the initial load reduces this risk.
Timers (
setInterval,setTimeout) that run heavy work at intervals.Overlapping interactions: processing one key press delays the next.
Third-party scripts (chat, analytics, ads, A/B tests) that run on the same thread. The LoAF
sourceURLandinvokerfields show which ones they are. For each one, consider whether the value it brings justifies the cost; if it stays, load it withasyncordefer, or later (web.dev).
How to reduce processing duration
The web.dev rule is to do as little as possible inside the event handler: update the interface first and defer the rest.
Update the screen and defer the rest
field.addEventListener('input', (event) => {
// What the user needs to see now
updateField(event);
// The rest waits until after the next frame
requestAnimationFrame(() => {
setTimeout(() => {
countWords();
checkSpelling();
saveDraft();
}, 0);
});
});This pattern, adapted from the web.dev example, works in every browser. requestAnimationFrame waits for the next frame and setTimeout moves the work to a new task, after the paint.
Yield during long work with scheduler.yield
When the work is a sequence of steps, such as processing a list, you can give control back to the browser between steps. scheduler.yield() pauses the task, lets the browser handle clicks and paint, and resumes your code with priority over other tasks in the queue (MDN). According to MDN compatibility data, it is available in Chrome and Edge 129 and in Firefox 142; Safari does not support it yet (MDN browser-compat-data, checked in October 2026). So use it with a fallback:
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
// Fallback for browsers without scheduler.yield
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function processItems(items) {
for (const item of items) {
process(item);
await yieldToMain();
}
}The function follows the model in the web.dev long tasks guide. The difference with the fallback: with setTimeout the continuation goes to the back of the queue, and other scripts (third-party ones included) can run before it. The code stays responsive, just without the resume priority. There is also the scheduler-polyfill package, mentioned in the same guide.
Avoid forced layout
Changing a style and then immediately reading a measurement, such as offsetHeight, forces the browser to recalculate layout in the middle of your code. Inside a loop, this multiplies the cost. Group the reads before the writes. In LoAF, this time shows up in forcedStyleAndLayoutDuration.
How to reduce presentation delay
After your code finishes, the browser still has to recalculate styles, run layout and paint. This cost grows with the size of the page:
A large DOM. Lighthouse warns above 800 nodes and flags it as excessive above 1,400 (web.dev). Remove unnecessary wrappers and insert hidden content (tabs, menus) only when the user asks for it.
Off-screen content rendered for nothing. The CSS property
content-visibility: autodefers rendering of sections that are not visible. It works in Chrome 85, Firefox 125 and Safari 18 onward (MDN browser-compat-data).A lot of HTML generated by JavaScript at once. The browser only paints after it has parsed and rendered all of it. Generate it in parts or deliver it ready from the server.
.footer-section {
content-visibility: auto;
contain-intrinsic-size: auto 600px; /* estimated height, adjust to your layout */
}Lab and field give different answers
Lighthouse does not measure INP, because it does not click anything. It reports TBT (Total Blocking Time), a load metric that correlates with INP but does not replace it (web.dev). A good Lighthouse score does not guarantee a good INP, and interactions after loading only show up if you test the flow by hand.
Coverage also differs in the field. CrUX only comes from Chrome users who opted in to sharing statistics, on desktop and Android (Chrome for Developers). The Event Timing API, which web-vitals uses to calculate INP, is available in Chrome, Firefox 89 and Safari 26.2 onward, but LoAF only in Chrome and Edge. In practice, your RUM measures INP in more browsers, but detailed per-script attribution only comes from Chromium browsers. The comparison between these sources is in Lighthouse, CrUX and RUM.
Fixing one slow interaction often reveals the next one. web.dev treats INP optimization as a cycle: measure in the field, reproduce in the lab, fix and measure again. LCP and CLS problems have their own guides: how to improve LCP and how to fix CLS.
How to check your site
In WebVyze, the INP debugger uses Google PageSpeed to show where the page stands, and the Core Web Vitals checker gives the CrUX INP for the last 28 days, on mobile and desktop, for up to 10 URLs. Both tools are free. On the paid plans, a real-visitor script based on the web-vitals library records the INP of each visit along with its cause.
Frequently asked questions
- Do scrolling or hovering count toward INP?
- No. INP observes clicks, taps and key presses. Hovering, scrolling and zooming are left out.
- What if the visitor does not click anything?
- Then the visit has no INP value. The same goes for someone who only scrolled the page and for bots that do not simulate interaction.
- Do interactions inside iframes count?
- They count toward the CrUX INP. Browser APIs, and therefore the web-vitals library, cannot see inside iframes, so your RUM may show a different value from CrUX on pages with iframes.
- Is isInputPending still useful for deciding when to yield?
- web.dev no longer recommends this API. It can return the wrong answer right after an interaction and does not account for visual updates that also need room to run. The current recommendation is scheduler.yield or scheduler.postTask.
See what your site needs to fix.
A free check by law, with the cause and the fix guide for each problem.
Keep reading
- Technical SEO
AI crawlers in robots.txt: search, user access and training
GPTBot, ClaudeBot, OAI-SearchBot, Google-Extended: what each AI crawler does, whether it respects robots.txt, and how to block training but stay in search.
- Technical SEO
Redirects: chains, loops and how to fix them
How Google handles 301, 302, 307 and 308, what chains and loops do to crawling and speed, and how to map, fix and test your redirects.
- Technical SEO
llms.txt: what it is, what it doesn't do and how to create one
llms.txt summarizes a site for AI agents. See the format, what Google and other companies say about it, when it's worth creating and how to write a good one.