How to fix a slow INP, step by step
A practical recipe for an Interaction to Next Paint over 200 ms: find the slow interaction, read its three parts, and fix the part that is longest.
In this guide
- Step 1: find out which interactions are slow
- Step 2: record the interaction in DevTools
- Step 3A: the wait to start is long (input delay)
- Step 3B: your handler takes long (processing duration)
- Step 3C: drawing the result takes long (presentation delay)
- Special cases
- Step 4: test again
- Check your own page
- Sources
Step 1: find out which interactions are slow
INP is a field metric, so the first job is to learn which interaction real visitors find slow. A lab test cannot tell you, because nobody taps anything in it. Look in three places, from least to most precise.
- Search Console and PageSpeed Insights tell you which pages have a poor INP in real-visitor data. How to measure Core Web Vitals shows where to read them. This answers which pages, not which interaction.
- Your own testing. Think of what visitors do on those pages: open the menu, switch a tab, add to the cart, type in a search box, accept the cookie banner. Try each, with the CPU slowed down in DevTools, on a real phone if you can.
- Real-user monitoring with attribution. The web-vitals library's attribution build reports, for the interaction that set the INP, the element that was touched (
interactionTarget), the three parts (inputDelay,processingDuration,presentationDelay), whether the page was still loading (loadState) and, in Chrome, the scripts of the long frames involved. A week of data usually shows that the same two or three interactions cause most of the slow results.
Step 2: record the interaction in DevTools
Open the Performance panel in Chrome DevTools, start a recording, perform the interaction once, and stop. Find the interaction on the timeline and read its breakdown. Three facts to take away:
- Which of the three parts is longest. That decides which section below to read.
- Which task is running underneath it. Select the long task and read its call stack: it names the scripts and functions.
- Whether a red mark is on a layout or style recalculation. That is a sign of forced layout, covered below.
Every frame on the page has its own main thread, including each iframe, and INP counts slow interactions in iframes toward the page. If the call stack points at a different origin, the slow code is not yours: an embedded widget, an ad or a chat tool.
Step 3A: the wait to start is long (input delay)
The main thread was busy when the visitor tapped. Find out what it was doing.
It was still starting up. After a script downloads, the browser has to parse it, compile it and run it, and that can take a long task on its own. A visitor who taps while the page is still loading waits for all of it. The cure is less JavaScript at startup: remove scripts you do not use, add defer, split large bundles so each page loads only what it needs (bundle size and code splitting), and load widgets when someone asks for them. The loadState value from the monitoring tells you whether slow interactions happen during loading. If they do, this is your problem.
A timer or a callback was running. Pollers that run every few hundred milliseconds, scroll and resize handlers that do real work on every event, and analytics code that processes each event can all be in the way. Look for repeated work under the long task and make it rarer, cheaper, or both.
A second interaction overlapped the first. Rapid typing or double taps queue work behind work. Keep each handler light so the queue never grows.
A third-party tag was running. Chat, ads, tag managers, session recording. The "3rd parties" insight in the report shows their share. Remove what you do not need and load the rest later.
Step 3B: your handler takes long (processing duration)
The browser started promptly, but your code took long to finish.
Do the visible part first, then yield. Only the code that changes what the visitor sees has to run before the next frame. Everything else can wait for the following task. A text field that updates a word count, checks spelling and saves a draft on every key press should update the field first and defer the rest. This pattern works in every browser: ask for the next animation frame, and start the rest of the work from a timer inside it.
input.addEventListener('input', (event) => {
updateTextBox(event); // what the visitor sees: do it now
requestAnimationFrame(() => {
setTimeout(() => { // the rest runs after the next frame
const text = input.value;
updateWordCount(text);
checkSpelling(text);
saveDraft(text);
}, 0);
});
});Break a long job into pieces. JavaScript runs each task to the end, and calling five functions in a row is one long task. Yielding to the main thread between them lets the browser handle input and draw. scheduler.yield() does it and, unlike a plain timer, resumes your code before other queued work, so the order is preserved. It runs in Chrome 129 and later and in Firefox 142 and later; use a timer where it is missing. More techniques are in breaking up long tasks, and work that does not need the page can run on another thread, as web workers explains.
Do not make the browser lay the page out repeatedly. (More on layout cost in DOM size and rendering.) Changing a style and then reading a layout value, such as an element's width or offset, in the same task forces the browser to calculate the layout right away. In a loop it can happen hundreds of times. Read all the values first, then write all the changes. In DevTools, forced layout shows up as a red mark on the style or layout steps.
Do less. Do not run an expensive handler on every key press: wait until typing pauses, or run it on a timer. Do not process the whole list when one item changed. Do not parse large data in the handler when it could be prepared earlier.
Step 3C: drawing the result takes long (presentation delay)
Your code was quick, but the browser needed a long time to draw the next frame.
A large page. The cost of updating a page grows with the number of elements, and not in a straight line. Every change can force style and layout work across thousands of elements. Reduce the size of the page, avoid deep nesting, and render long lists only as far as the visitor has scrolled.
Off-screen content. content-visibility: auto on long sections lets the browser skip drawing them until they near the screen. Set contain-intrinsic-size too, so that the page does not shift when they appear.
Rendering HTML in the browser after a tap. A tap that makes a script build a large block of HTML has to parse it, style it and lay it out before the next frame. Build large blocks on the server where you can, or add to the page in smaller steps.
Expensive visual effects. Large blurs, shadows and filters on big areas, and animations of layout properties, cost more per frame. Prefer transforms and opacity.
Special cases
Single-page applications. A route change is an interaction like any other, and everything the visitor does in the visit counts toward the one INP of that page load. Heavy hydration at startup and large re-renders on each tap are the usual causes. Render less on each interaction, and split the code by route.
WordPress and page builders. Look at which plugin scripts run on the page. Sliders, mega menus, animation libraries and form plugins often run on every page, though the feature is on one. Many optimization plugins offer to delay JavaScript until the first interaction. That moves the cost to the first tap: all the delayed scripts run at once, which can make the first interaction very slow. Test it with real taps before you keep it.
Embeds. Maps, videos and social widgets are often cheap until they are used. Show a static preview and load the real thing on a click.
Step 4: test again
- Repeat the recording and compare the three parts. The one you worked on should be shorter.
- Try the same interaction at a slower CPU setting.
- Use the page for a few minutes the way a visitor would, and watch for new slow spots.
- Check the real-user data in four weeks: INP only changes as real visitors interact.
Check your own page
Run the free audit to see whether real visitors get a good INP. If not, record two or three of the interactions people use most, and compare the three parts. The Total Blocking Time guide covers the loading side of the same problem.
Sources
- Optimize Interaction to Next Paint on web.dev: input delay, event callbacks, layout thrashing, DOM size and
content-visibility. - Optimize long tasks on web.dev: breaking up tasks and yielding, including
scheduler.yield(). - The web-vitals library: the attribution build and its INP fields.
- Scheduler: yield() on MDN: browser support.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Core Web Vitals · Book a call