Core Web Vitals 2026 - How to Master INP (Interaction to Next Paint)
INP is the Core Web Vital most sites fail. Learn what INP measures, the 200ms threshold, and concrete JavaScript techniques to make your store feel instant.

A client pinged me in a panic because Search Console had flagged INP red across their entire product catalog — overnight, no deployment, nothing changed on their end. That's when I really dug into what INP actually demands, and honestly, I've been auditing for it on every project since.
INP — Interaction to Next Paint — is the Core Web Vital with the lowest pass rate. Roughly 43% of websites still fail its 200ms threshold, which makes it the most commonly missed signal in Google's ranking algorithm. Unlike its predecessor FID, which was easy to game, INP demands real JavaScript architecture work. Here's what it measures and how to fix it.
What INP Actually Measures
INP captures the full interaction lifecycle from the moment a user clicks, taps, or presses a key to the moment the browser finishes painting the resulting visual change. That lifecycle has three distinct phases:
- Input delay — time from user gesture to when the event handler starts (blocked by other tasks on the main thread)
- Processing time — time the event handler itself takes to run
- Presentation delay — time from handler completion to the next frame being painted
FID only measured input delay and only for the very first interaction. INP measures all three phases for every interaction across the entire page visit, then reports the worst-case interaction at the 75th percentile of your real users.
I've found that the presentation delay phase catches the most people off guard. You profile your handler, it looks fast, but the browser still takes 400ms to repaint. That's usually a layout thrash or a style recalculation triggered by the handler — something FID would have completely missed.
Thresholds
| Score | INP |
|---|---|
| Good | ≤ 200ms |
| Needs Improvement | 200 – 500ms |
| Poor | > 500ms |
"Good" means at least 75% of user interactions complete within 200ms. A single sluggish filter dropdown can push your entire site into the red.
My rule of thumb: if a filter or accordion takes a beat to respond on a mid-range Android phone, you're already over 200ms for a big chunk of your users. Don't benchmark on your M3 MacBook.
Why So Many Sites Fail It
FID was generous — it only cared about one interaction and ignored processing time entirely. Most sites passed with minimal effort. INP changed the contract entirely. Now every accordion, form submission, dropdown, and filter application is scored. Third-party scripts (analytics, chat widgets, ads, social embeds) compete for the same main thread, extending your input delay without you writing a single line of bad code.
I'll be honest — third-party scripts are always the silent killer. Every time I see a poor INP score on an otherwise clean codebase, the first thing I check is the waterfall of third-party tags loading on page. It's almost never the product code.
The business stakes are real: Google's Web Vitals team found improving INP from 500ms to 200ms correlates with roughly a 22% improvement in user engagement metrics — and it's an official ranking signal. Sites that coast on their old FID pass are now actively losing ground.
If you're working on a Shopware store, this compounds the server-side tuning covered in the Shopware 6 performance guide — fast TTFB doesn't help if the main thread is jammed after load.
How to Measure INP
Field data (real users) is the only authoritative source for INP because it depends on actual interaction patterns:
- PageSpeed Insights — real-user CrUX data for any URL, plus lab diagnostics
- Google Search Console — Core Web Vitals report with 28-day rolling field data
- Chrome UX Report (CrUX) — raw dataset via BigQuery or API
web-vitalsJS library — instrument your own users in production
Add this to your Nuxt/TS app to capture INP in real-time:
import { onINP } from 'web-vitals'
onINP(({ value, rating, attribution }) => {
console.log(`INP: ${value}ms (${rating})`)
// Send to your analytics endpoint
navigator.sendBeacon('/api/vitals', JSON.stringify({
metric: 'INP',
value,
rating,
element: attribution.interactionTargetElement?.tagName,
}))
})
That attribution.interactionTargetElement is gold — it tells you exactly which DOM element triggered the slow interaction. I've caught filter checkboxes, autocomplete inputs, and quantity steppers this way. Far faster than guessing.
Lab tools (Lighthouse, Chrome DevTools Performance panel) can't measure INP directly — there's no scripted interaction to record. Use them to spot long tasks and trace event handler costs, then validate in the field.
After deploying fixes, allow 2–4 weeks before Search Console reflects the improvement — it runs on a 28-day rolling window. I always warn clients upfront: don't panic when the graph doesn't move the day after a deploy.
Optimization Playbook
Yield Long Tasks
Any task running longer than 50ms blocks the main thread. Break it up and yield between chunks so the browser can process pending input events:
async function processLargeList(items: Item[]) {
for (const item of items) {
processItem(item)
// Yield to the main thread after each item
await scheduler.yield()
}
}
scheduler.yield() is the cleanest API. For broader browser support, use a setTimeout(0) fallback:
const yieldToMain = () =>
'scheduler' in window
? scheduler.yield()
: new Promise<void>(resolve => setTimeout(resolve, 0))
In my experience, adding a single scheduler.yield() call inside a cart recalculation loop dropped a client's checkout INP from over 600ms to under 180ms. Sometimes one line is all it takes — the work was already there, it just wasn't letting the browser breathe.
Code-Split Heavy Bundles
Large JS payloads inflate processing time. Ship only what's needed for the current interaction using dynamic import():
button.addEventListener('click', async () => {
const { openModal } = await import('./modal')
openModal()
})
This defers parsing and execution of the modal module until the user actually triggers it, keeping the initial bundle lean.
I reach for this pattern every time I see a heavy modal or multi-step form that only a fraction of users will open. No reason to pay that parse cost upfront.
Offload Work to Web Workers and requestIdleCallback
CPU-heavy work (search indexing, data transformation, complex calculations) should never block the main thread:
// Heavy computation in a worker
const worker = new Worker(new URL('./search.worker.ts', import.meta.url))
worker.postMessage({ query: inputValue })
worker.onmessage = ({ data }) => renderResults(data.results)
// Non-critical work during browser idle time
requestIdleCallback(() => {
prefetchNextPageData()
}, { timeout: 2000 })
Web Workers are one of those things I avoided for years because the setup felt fiddly. Once I started using them for faceted search indexes, I wondered why I'd waited so long. The main thread is just cleaner.
Defer and Facade Third-Party Scripts
Third-party scripts are the silent INP killers. Don't load them until they're actually needed:
// Facade: show a static placeholder, load the real widget on interaction
chatButton.addEventListener('click', async () => {
const script = document.createElement('script')
script.src = 'https://cdn.chatwidget.example/widget.js'
document.head.appendChild(script)
script.onload = () => window.ChatWidget.open()
}, { once: true })
For analytics and ad scripts, load them after the page is interactive:
<!-- Defer non-critical scripts entirely -->
<script src="https://analytics.example/tracker.js" defer></script>
The facade pattern feels slightly hacky the first time you implement it, but it's the right call. Users who never click the chat button never pay for that widget's JavaScript at all.
Debounce Handlers and Use Passive Listeners
Expensive event handlers (search-as-you-type, scroll-triggered updates) must be debounced. Scroll and touch listeners should be passive to avoid blocking gesture handling:
const debounce = <T extends unknown[]>(fn: (...args: T) => void, delay: number) => {
let timer: ReturnType<typeof setTimeout>
return (...args: T) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
const handleSearch = debounce(async (query: string) => {
const results = await fetchSearchResults(query)
renderResults(results)
}, 200)
// Passive listener — tells the browser this handler won't call preventDefault()
document.addEventListener('touchstart', onTouch, { passive: true })
The passive flag is one of those small wins that costs nothing to add. Every time I skip it and someone later profiles a scroll-heavy page, it shows up. I just add it by default now.
For Vue/Nuxt Stores Specifically
Nuxt's SSR → hydration cycle is a prime INP risk window. During hydration, Vue is replaying event bindings and component setup on top of server-rendered HTML. Any user interaction that lands during this window will experience inflated input delay.
What burned me once was a heavy product listing component that eagerly set up several watchers during hydration — users on slower connections who interacted before hydration finished would get 700ms+ responses. It looked fine in Lighthouse. It only surfaced in field data.
Key tactics:
- Lazy-load heavy components with
<LazyMyComponent />(Nuxt's auto-import prefix) ordefineAsyncComponent. The Nuxt 4 migration guide covers the updated lazy hydration APIs worth adopting. - Defer non-critical composable setup — move analytics initialization, A/B test evaluation, and prefetch logic into
onNuxtReadyor arequestIdleCallbackcall insideonMounted. - Avoid large synchronous
watchhandlers on reactive state tied to UI interactions — split them intowatchEffectwith a yield, or offload to a worker. - Check your hydration payload size — oversized
useAsyncDatapayloads inflate the JS parse budget and delay when the page becomes interactive.
Going headless with Shopware + Nuxt already removes a large chunk of server-rendered PHP overhead from the equation; the remaining INP budget is entirely yours to control on the frontend.
Conclusion
INP is the performance metric that separates sites built for real users from sites built to pass a one-time audit. The fixes aren't glamorous — yield the main thread, split bundles, defer third parties — but they're concrete and the payoff is measurable. Many sites see a 30–50% INP improvement in under eight hours of focused work.
I've gone through this process enough times now to know the pattern: field data reveals the worst offender, DevTools traces it, one of the techniques above fixes it. Repeat until you're green. It's not magic, it's just methodical.
If your store is running JavaScript-heavy interactions and hasn't been audited for long tasks, start with PageSpeed Insights field data today. Find the worst-offending interaction, trace it in DevTools, and apply the yield pattern. The ranking and engagement gains follow from there.
Need a targeted performance audit or a headless frontend that starts with good INP by design? Get in touch.
Useful References
Enjoyed this?
Get new posts as they land.
Keep reading

Purging Cloudflare's Cache Automatically on Every Netlify Deploy
This site sits on Netlify behind Cloudflare, and after every deploy Cloudflare kept serving stale HTML. Here's why the Netlify-CDN-Cache-Control header was a dead end for static files, and the 26-line build plugin that purges Cloudflare's edge cache the moment a deploy goes live — including the CJS/ESM gotcha that broke the first build.

Debugging LCP on a Real Site - A Case Study on marcofaul.de
A first-person walkthrough of fixing LCP on this very site - a JS reveal animation gating first paint, a font preload that held rendering hostage, a 5.8s image CDN cold start, and the measurement noise that almost sent me chasing ghosts.

Shopware 6 Developer Performance Guide - Essential Improvements and Fixes
Performance optimization is crucial for any e-commerce platform. Here are the most impactful performance improvements and fixes for Shopware 6 developers.