INP Optimisation: How to Fix Interaction to Next Paint
Have you opened a Core Web Vitals report recently and seen a perfectly green FID column quietly replaced by a slightly redder INP column? You are not alone. On 12 March 2024, Google replaced FID with INP as the responsiveness Core Web Vital, and a lot of sites that were comfortably passing for years suddenly are not.
INP is not just FID with a new name. It is a meaningfully harder bar, and the techniques that "fixed" FID often do nothing for INP - or actively make it worse. This is the playbook I use when I audit a site for INP, the eight fixes that move the needle in roughly the order I apply them, and the third-party scripts I always look at first.
The quick answer: what is INP and how do I improve it?
INP (Interaction to Next Paint) measures the longest delay between a user interaction (click, tap, key press) and the next visual update on the page, across the whole session. A good INP score is 200ms or less. To improve INP, reduce long JavaScript tasks on the main thread, defer or delay third-party scripts, and yield to the browser between expensive operations using scheduler.yield() or setTimeout(0).
That sentence summarises the whole post. The rest of this guide unpacks each part with the specific code patterns and audit steps I use.
INP vs FID: what actually changed in March 2024
FID (First Input Delay) only measured the delay between the user's first interaction and the moment the browser could begin processing it. INP measures the full round trip - input event to next visual update - across every interaction in the session, then reports a value close to the worst one (technically the 98th percentile for sessions with many interactions).
The implications are big:
- A site can have great FID and terrible INP. FID never cared about the visual update, only the input processing delay. INP penalises slow renders too.
- A site can have a fast first interaction and a slow tenth. FID ignored the tenth. INP captures it.
- Server-side rendered sites used to look great on FID. That advantage disappears with INP because the bottleneck is now post-hydration JavaScript, not initial input handling.
The Chrome team's launch post frames INP as "the responsiveness CWV", which is exactly the right way to think about it. FID was a proxy for responsiveness. INP measures it directly.
What counts as a "good" INP score?
A good INP score is 200ms or less. Google's thresholds are: under 200ms = good, 200-500ms = needs improvement, over 500ms = poor. Like other Core Web Vitals, the threshold is measured at the 75th percentile of all page loads, using real user data from the Chrome User Experience Report.
The 200ms target is tighter than it sounds. A single long JavaScript task on a click handler can blow past it. A third-party chat widget initialising on first interaction can blow past it. A React component doing too much work in a click handler can blow past it.
The CrUX-backed CWV technology report consistently shows INP as the hardest of the three Core Web Vitals for most sites to pass, especially for content-heavy or e-commerce sites that lean on third-party scripts. If you have an INP-related ranking concern, you are in good company - it is genuinely the toughest one.
The top 5 causes of bad INP
The five most common causes of bad INP, in the order I usually find them: long JavaScript tasks blocking the main thread, third-party scripts (analytics, chat widgets, A/B testing, ads) hogging the main thread during interactions, large DOM updates that trigger expensive layout work, sync work inside event handlers that should be async, and re-renders cascading through component trees in React or other heavy frameworks.
Each of these has a specific signature you can spot in the Performance panel:
- Long tasks show up as orange-bordered rectangles in the main thread track, anything over 50ms
- Third-party scripts appear under their domain in the Network and Performance panels - you can usually spot them by host (e.g.
intercom.io,googletagmanager.com,hotjar.com) - Layout thrashing appears as alternating purple "Layout" bars - if you see more than two in a row in an interaction, you have a problem
- Sync work in handlers appears as a single long yellow "Scripting" block triggered by a click event
- Cascading re-renders look like a series of small but cumulative scripting tasks following an interaction
Knowing the signature lets you skip straight to the fix instead of generic "speed up your JavaScript" advice.
How to measure INP on your site
To measure INP, open PageSpeed Insights, paste your URL, and look at the "Discover what your real users are experiencing" section for field data (real CrUX data), or use Chrome DevTools' Performance panel and the web-vitals.js library to capture INP in your own monitoring. Field data is the score that affects ranking; lab data tells you what to fix.
The four ways to measure INP in practice:
- PageSpeed Insights - free, gives you field + lab data, the most accessible starting point
- CrUX dashboard in Search Console - shows real user INP across your whole site over time
- DevTools Performance panel - record an interaction, look at the "Long animation frames" track, identifies the specific task that caused the delay
- web-vitals.js - drop into your site, ship INP measurements to your own analytics for full visibility
I always start with PageSpeed Insights for a snapshot, then go to DevTools Performance with the interaction I am trying to fix. The Performance panel's "Long animation frames" view, added in 2024, is purpose-built for INP debugging and is the single most useful tool I know for this.
The INP optimisation playbook: 8 fixes ordered by impact
These are the eight fixes I run through on every INP audit, in roughly the order of impact-to-effort ratio:
1. Defer non-critical third-party scripts
Most chat widgets, analytics, A/B testing tools, and ad scripts do not need to load on first paint. Defer them until idle using requestIdleCallback, lazy-load on interaction, or use a tag manager that supports conditional loading. This single change often improves INP by 50-150ms on script-heavy sites.
2. Break up long tasks with scheduler.yield()
If you have a click handler doing 200ms of work, the browser cannot paint until it finishes. Use scheduler.yield() (or setTimeout(0) as a fallback) to give the browser a chance to paint between steps:
async function handleClick() {
doFirstPart();
await scheduler.yield(); // browser can paint here
doSecondPart();
await scheduler.yield();
doThirdPart();
}
This is the single most powerful new technique INP introduced. The work still happens. The browser just gets to render between chunks.
3. Debounce expensive input handlers
Search-as-you-type, filtering, real-time validation - any handler that fires on every keystroke is a prime INP target. Wrap in a debounce (200-300ms) so the work fires once after the user stops typing, not on every key.
4. Use CSS containment for expensive components
Add contain: content or contain: layout style to large card grids, lists, and product catalogues. This tells the browser that style and layout changes inside the contained element cannot affect anything outside it, dramatically cutting the layout cost of updates.
5. Virtualise long lists
If you render 1,000 table rows, every interaction with a row triggers a render pass over all 1,000. Virtualise with a library that only renders the visible window (TanStack Virtual, react-window, etc.) and the cost of any one interaction drops from O(n) to O(visible).
6. Optimise React re-renders
In React specifically, the most common INP killer is a parent re-render that cascades through hundreds of child components. Memoise expensive children with React.memo, stabilise prop references with useCallback / useMemo, and split context providers so a change in one slice does not invalidate the whole tree.
7. Move expensive work off the main thread
For genuinely heavy work (image processing, large data transforms, search indexing), use a Web Worker. The main thread stays free for input handling and rendering, INP stays under 200ms even during heavy work.
8. Avoid synchronous storage and DOM reads in handlers
Reading from localStorage, accessing offsetHeight, or calling getBoundingClientRect in a click handler forces layout and blocks the main thread. Batch these reads, do them outside hot paths, or use IntersectionObserver / ResizeObserver to compute them async.
The order matters because steps 1-3 are usually the biggest wins and the easiest to apply. Steps 4-8 are framework- or pattern-specific and require deeper changes. If you only have an afternoon, do 1-3.
Third-party scripts: the silent INP killer
Third-party scripts are the single biggest cause of bad INP on the sites I audit. Analytics, chat widgets, heat map tools, A/B testing platforms, advertising scripts - each one runs JavaScript on the main thread, and most run aggressively during the first interactions on a page (form opens, modal triggers, navigation clicks).
The diagnostic check that takes 30 seconds:
- Open DevTools Performance panel
- Hit Record
- Click a button on your page
- Stop recording
- Look at the main thread track during the interaction
- Any script blocks NOT under your own domain are third-party
If you see a 200ms block under intercom.io while the user is clicking your "Get a quote" button, that is your bad INP, full stop.
The fixes ranked by aggressiveness:
- Best: defer the script until after first interaction (load on
requestIdleCallbackor first scroll) - Better: load via a tag manager with conditional rules (only load chat on
/contact, only load A/B testing on tested pages) - Acceptable: load with
deferorasyncattribute and accept the cost - Worst: load synchronously in
<head>(this is still the default for most analytics tools by copy-paste)
I have seen e-commerce sites cut INP by 300ms by doing nothing more than moving their three biggest third-party scripts behind a "load on idle" wrapper. That is the difference between failing and passing.
Framework-specific INP wins
Each major frontend framework has its own INP profile and its own set of high-leverage fixes:
- React - the biggest wins are memoisation, splitting context providers, and React 19's
useTransitionfor marking non-urgent updates so they do not block input - Vue -
defineAsyncComponentfor lazy-loading expensive components, plusshallowReffor state that does not need deep reactivity - Svelte - generally has the best out-of-the-box INP because it ships less runtime, but custom store subscriptions can still cause cascading updates worth profiling
- Astro - islands architecture is INP-friendly by design; the trap is that
client:loadon too many components reintroduces the same problem React has on its own
The general principle holds across frameworks: the cheapest interaction is the one that re-renders the least. Frameworks differ in how much they let you control that, but the goal is the same.
For more on where these performance signals fit into a wider audit, the complete guide to Core Web Vitals covers all three CWVs together, and the Core Web Vitals explained explainer is a good starting point if any of this is new.
Common mistakes that make INP worse
The five INP mistakes I see most often, all of which feel reasonable until you measure them:
- "Just add it to the page" for new third-party tools. Every script you add costs INP. The marketing team's new heat map tool, the support team's new chat widget, the growth team's new A/B testing platform - all of them. None of them are free.
- Loading frameworks twice. I see this surprisingly often on multi-tenant or migration sites where two versions of React, jQuery, or a UI library coexist. Double the runtime, double the INP cost.
- Using
requestAnimationFrameto "fix" INP. It does not. RAF runs at frame boundaries, but if the task itself is long, it still blocks. Usescheduler.yield()instead. - Optimising LCP at the cost of INP. Aggressive preloading, large bundle splitting, and synchronous render-blocking critical CSS can all hurt INP if the JS they unblock is heavy.
- Trusting lab data alone. Lighthouse INP estimates are based on simulated interactions. Field data from CrUX is the score that matters for ranking, and the two can differ significantly.
The state of CWV passing rates is documented in only 56% of websites pass Core Web Vitals, and INP is the biggest reason that number has not climbed. If your site is in the failing 44%, INP is the most likely culprit in 2026.
The bottom line
INP is a tighter, fairer responsiveness metric than FID. It also exposes the cost of JS-heavy front-ends in a way the old metric never did. The good news is that the fixes are well-understood: defer third-party scripts, break up long tasks with scheduler.yield, debounce hot input handlers, virtualise long lists, and stop blocking the main thread inside event handlers.
If you want the broader context on where INP sits among the other Core Web Vitals (LCP, CLS) and how they all roll up into a single ranking signal, the Core Web Vitals pillar guide covers the lot. And if you want INP measured automatically across your whole site alongside the other 5 pillars of website health, Kritano runs the audit and tracks scores over time so you can see exactly what is getting better and what is regressing.
Either way, the next 30 minutes spent in DevTools' Performance panel staring at a Long Animation Frames track on your own site will teach you more about your INP problems than any audit report.
FAQs
What is INP?
INP (Interaction to Next Paint) is a Core Web Vital that measures the longest delay between user interactions (clicks, taps, key presses) and the next visual update on the page, across the whole session. It replaced FID on 12 March 2024 as Google's official responsiveness metric. Unlike FID, INP captures every interaction, not just the first one, and includes the time it takes the browser to paint the result.
What is a good INP score?
A good INP score is 200 milliseconds or less. Google's thresholds are: under 200ms = good, 200-500ms = needs improvement, over 500ms = poor. The score is measured at the 75th percentile of real user interactions from the Chrome User Experience Report, so passing INP means at least 75% of your users see interactions complete in under 200ms.
How is INP different from FID?
FID only measured the delay before the browser could begin processing the first user interaction. INP measures the full latency (input plus processing plus paint) of every interaction in the session and reports a value close to the worst. INP is a meaningfully harder bar to pass because it includes the visual update time and captures all interactions, not just the first.
What causes a bad INP score?
The most common causes of a bad INP score are long JavaScript tasks blocking the main thread, third-party scripts (analytics, chat widgets, A/B testing) hogging the main thread during interactions, heavy DOM updates that trigger expensive layout work, synchronous storage or layout reads inside event handlers, and cascading re-renders in component-heavy frameworks like React. Third-party scripts are the single most common cause on the sites I audit.
How do I check my INP?
Check your INP using PageSpeed Insights (paste your URL, look at the field data section), Search Console's Core Web Vitals report (real user data over time), Chrome DevTools' Performance panel with the Long Animation Frames track, or by adding the web-vitals.js library to your site to capture INP in your own analytics. PageSpeed Insights is the fastest starting point; DevTools is the best tool for debugging specific slow interactions.

Founder of Kritano
5 years in web development. I specialise in web auditing, WCAG 2.2 compliance, and search engine optimisation.
I built Kritano after years of running audits with fragmented tools. I write about SEO, accessibility, security, and performance based on real auditing data from thousands of scans.