Web Development · Performance
INP Is a Core Web Vital Now. Here Is What's Still Failing and How to Fix It.
Interaction to Next Paint replaced First Input Delay in 2024, and most sites still haven't caught up. INP is harder to optimize because it measures every interaction, not just the first one.
Anurag Verma
8 min read
Sponsored
In March 2024, Google replaced First Input Delay (FID) with Interaction to Next Paint (INP) as a Core Web Vital. FID had been easy to game — it only measured the delay before the browser started processing the first user interaction, not whether the result actually appeared on screen. INP is harder. It measures the full interaction: from when the user taps, clicks, or presses a key, to when the browser paints the visual response. And it takes the worst interaction in the session, not the first.
Two years later, many sites are still failing it. The Chrome User Experience Report data for 2026 shows that around 35% of origins still have “Needs Improvement” or “Poor” INP scores. If you haven’t specifically optimized for INP, you probably have problems you haven’t found yet.
What INP Measures
INP captures three types of interactions: clicks, taps, and keyboard presses. It does not measure scrolling or hovering. For each interaction, it measures the time from input event to the next paint.
The thresholds from Google:
- Good: under 200ms
- Needs Improvement: 200-500ms
- Poor: over 500ms
The “worst interaction” rule has nuance. INP uses the 98th percentile of interaction durations in the session, which filters out one-off outliers without ignoring recurring problems.
An INP of 300ms means the 98th percentile interaction in your session takes 300ms to show visual feedback. A user clicking a button and waiting a third of a second to see anything is a noticeable delay.
Why It’s Harder Than FID
FID only asked: “When the user first clicked, how long did the browser wait before starting to process it?” Passing FID required not blocking the main thread during page load. One change (usually breaking up a large JS bundle or deferring non-critical code) could fix FID for most sites.
INP asks: “For every click, tap, or keypress throughout the session, how long until the visual update appears?” This includes:
- Interactions after heavy data loads
- Interactions that trigger React re-renders of large trees
- Keyboard interactions in input-heavy UIs
- Interactions that fire analytics or third-party scripts
The main thread blocking that FID missed — the kind that happens after page load, during user activity — is exactly what INP catches.
The Three Parts of an Interaction
An interaction’s duration is the sum of three phases:
Input delay: The time between the user’s input and when the browser starts processing the event handler. This is what FID measured. It’s high when the main thread is busy with something else — a long task running when the user clicks.
Processing time: How long your event handler takes to run. A click handler that does a lot of synchronous work (filtering a large array, building a DOM tree, computing a layout) has high processing time.
Presentation delay: The time between when your JavaScript finishes and when the browser actually paints. This phase is dominated by browser layout and rendering work — triggered when JavaScript changes the DOM in ways that require recalculation.
Total INP = input delay + processing time + presentation delay.
Optimizing INP means reducing whichever of these is the bottleneck for your worst interactions.
Finding Your INP Issues
The fastest tool for a quick diagnosis is the Chrome Performance panel. Record a session, interact with your page, and look for long tasks and layout thrashing in the main thread timeline.
For field data, CrUX (Chrome User Experience Report) has INP via PageSpeed Insights and Google Search Console. Field data is more useful than lab data for INP because it captures real user behavior patterns, including interactions that happen after the page settles.
Chrome’s web-vitals library reports INP in the browser:
import { onINP } from 'web-vitals'
onINP((metric) => {
console.log('INP:', metric.value, 'ms')
console.log('Attribution:', metric.attribution)
// Send to your analytics
sendToAnalytics({
name: 'INP',
value: metric.value,
// metric.attribution tells you which interaction was the worst
elementTarget: metric.attribution.interactionTarget,
eventType: metric.attribution.interactionType,
})
})
The interactionTarget from attribution gives you the CSS selector for the element that caused the poor interaction. Combined with interactionType, you can pinpoint exactly what’s slow: “button.checkout-submit click” is more actionable than just knowing your INP is 450ms.
Common Causes and Fixes
Long Event Handlers
The most common INP problem: a click or input handler that does too much synchronous work.
// Slow: synchronous filter over a large list
searchInput.addEventListener('input', (e) => {
const query = e.target.value
const results = largeList.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
)
renderResults(results)
})
For expensive operations, defer work that doesn’t need to happen synchronously:
// Better: yield to the browser before the expensive work
let debounceTimer
searchInput.addEventListener('input', (e) => {
const query = e.target.value
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
const results = largeList.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
)
renderResults(results)
}, 16) // roughly one frame
})
For interactions that must be immediate, break processing into microtasks:
async function handleClick() {
updateUIImmediately() // First: update what the user can see now
// Yield to browser paint
await scheduler.yield()
// Then do expensive follow-up work
await expensiveUpdate()
}
scheduler.yield() is a relatively new browser API that yields to the browser’s task queue, allowing a paint before continuing. It’s supported in Chrome and has a polyfill via setTimeout(resolve, 0).
React Re-renders of Large Trees
React apps with large component trees can have high processing time for interactions that trigger re-renders.
Memoize expensive components so they don’t re-render when parent state changes:
// Without memo: re-renders whenever parent re-renders
function ExpensiveList({ items }) {
return <div>{items.map(item => <Item key={item.id} {...item} />)}</div>
}
// With memo: skips re-render when items hasn't changed
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
return <div>{items.map(item => <Item key={item.id} {...item} />)}</div>
})
Separate frequently-updated state from infrequently-updated state. If you have a modal that tracks whether it’s open and also contains a large list, moving the modal state closer to the button that toggles it prevents the list from re-rendering on open/close.
Use useTransition for non-urgent updates in React 18+:
import { useTransition } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleSearch(e) {
const value = e.target.value
setQuery(value) // Urgent: update the input immediately
startTransition(() => {
// Non-urgent: update results in the background
// React can interrupt this if a more urgent update comes in
setResults(computeResults(value))
})
}
return (
<>
<input value={query} onChange={handleSearch} />
{isPending ? <Spinner /> : <ResultList results={results} />}
</>
)
}
useTransition marks the results update as non-urgent. React renders the input update first (keeping the typing responsive), then renders the results. If the user keeps typing before results are ready, React cancels the in-progress results render and starts over with the new query.
Third-Party Scripts
Third-party analytics, chat widgets, and ad scripts are a significant source of INP problems. They run on the same main thread as your application code, and their long tasks block your event handlers.
The most reliable fix is loading them with type="module" or defer and prioritizing your own event handling:
<!-- Defer non-critical third-party scripts -->
<script defer src="https://example-analytics.com/tracker.js"></script>
For scripts that must run synchronously, consider loading them after setTimeout or using the Partytown library to move them to a web worker entirely.
Layout Thrashing
Presentation delay spikes when JavaScript reads and then writes to the DOM in alternating cycles, forcing the browser to recalculate layout repeatedly (thrashing).
// Bad: reads and writes interleaved = layout thrash
elements.forEach(el => {
const height = el.offsetHeight // Read: forces layout
el.style.height = height + 10 + 'px' // Write: invalidates layout
})
// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight) // All reads first
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px' // All writes after
})
The browser’s DevTools Performance panel shows forced synchronous layout warnings when this is happening.
Prioritizing What to Fix
Not all INP problems are equally worth fixing. Focus on:
- High-traffic interactions: A slow checkout button affects more users than a slow settings toggle
- Interactions > 500ms: These are “Poor” INP scores and carry the most weight in Google’s ranking signal
- Interactions that feel broken: Users retry interactions they think failed, compounding the problem
The web-vitals attribution data showing interactionTarget in your analytics is the fastest path to prioritization. Once you’re tracking it, you’ll see which element and which interaction type is causing your worst scores in production.
INP is harder to pass than FID because it watches the whole session. But the problems it catches — slow event handlers, heavy re-renders, main thread contention from third parties — are exactly the things users notice and complain about. Fixing them improves how the app feels, not just how it scores.
Sponsored
More from this category
More from Web Development
OAuth 2.0 and PKCE: The Web Auth Patterns Every SPA Developer Needs in 2026
Technical SEO for JavaScript Apps in 2026: What Google Actually Renders
Progressive Web Apps in 2026: What Actually Works on iOS and Android
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored