Fixing Core Web Vitals INP in 2026: A Real Production Case Study

INP replaced FID as a Core Web Vital in 2024 and is now a confirmed ranking signal. Here is how we cut INP from 480ms to 95ms on a real client site.

In March 2024, Google replaced First Input Delay (FID) with Interaction to Next Paint (INP) as a Core Web Vital. By 2026, INP is the metric most sites are still failing on, and the data shows that fixing it directly improves search ranking, conversion, and user retention.

This post is a real before-and-after from a client project we shipped in February 2026. The site is a Vue 3 e-commerce platform doing about $200K/month in revenue. Their INP at 75th percentile was 480ms (well into the "poor" range). After the work below, it dropped to 95ms (well into "good"). Search impressions for product pages climbed 14% in the following 6 weeks.

What INP Actually Measures

INP is the longest delay between any user interaction (click, tap, key press) and the next paint that visibly responds, measured at the 75th percentile across the page session. The thresholds:

  • Good: below 200ms.
  • Needs improvement: 200ms to 500ms.
  • Poor: above 500ms.

Unlike FID, which only measured the first interaction, INP measures throughout the page lifetime. A site can pass FID and still fail INP because a click on the third menu item triggers a 700ms hang.

How We Diagnosed the Problem

Started with the Chrome DevTools Performance panel, but the real diagnosis came from real-user data. We added the web-vitals library to the site:

import { onINP } from 'web-vitals'

onINP((metric) => {
  navigator.sendBeacon('/api/rum', JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
    target: metric.attribution?.eventTarget,
    type: metric.attribution?.eventType,
    url: location.pathname
  }))
})

After 24 hours of real user data, the INP problems clustered in three places:

  1. The Add to Cart button on product pages (median 380ms, p99 1100ms).
  2. The mobile filter sidebar on category pages (median 240ms, p99 800ms).
  3. The Search input on the header (median 180ms, p99 420ms).

Fix 1: Add to Cart (Synchronous Vuex Mutation)

The Add to Cart button was running a synchronous Vuex mutation that recalculated cart totals, fired analytics, and re-rendered the entire cart icon component. All on the main thread, all before the next paint.

The fix was a yieldy pattern: do the bare minimum visible work first (show a spinner, update local count), then yield to the browser, then do the rest in the next frame.

async function addToCart(product) {
  // Visible feedback immediately
  showSpinner()
  cartCount.value++

  // Yield so the browser paints the spinner
  await new Promise(resolve => requestAnimationFrame(resolve))

  // Now do the heavy work
  await store.dispatch('cart/add', product)
  trackAnalytics('add_to_cart', product)
  updateRecommendations()
}

INP on Add to Cart dropped from 380ms to 80ms.

Fix 2: Mobile Filter Sidebar (Long Render)

The mobile filter sidebar rendered 200+ filter checkboxes on open. Each one was a Vue component with v-model bindings. Tapping the open button took 240ms because Vue had to mount all 200 components before the next paint.

Two fixes layered on top of each other:

  • Lazy-render filter sections. Only render the first three sections (price, brand, category) on initial open. The rest render after a microtask, so the sidebar paints first.
  • Replace per-checkbox v-model with a single reactive object. Reduced the reactive surface from 200 sources to 1.

INP on filter open dropped from 240ms to 70ms.

Fix 3: Header Search (Layout Thrash)

The search input fired a debounced query handler, but the handler also synchronously measured the input's getBoundingClientRect() to position the dropdown, then wrote a CSS variable, then read another rect, then wrote a class. Classic layout thrash, the browser did 3 forced reflows per keystroke.

Fix: batch all reads first, then all writes. Used fastdom as the boilerplate but the principle is just: separate read phase from write phase.

INP on Search dropped from 180ms to 45ms.

What We Did Not Need to Do

  • We did not switch frameworks.
  • We did not add a service worker.
  • We did not lazy-load anything that was not already lazy.
  • We did not pay for a CDN upgrade.

Most INP problems are not bandwidth or CDN issues. They are main-thread issues. The fix is almost always: do less work synchronously in the interaction handler, yield to the browser, then continue.

The Tools We Used

  • web-vitals npm package for real user measurement. Free, 4KB.
  • Chrome DevTools Performance panel with the "Interactions" track turned on.
  • Long Animation Frames API (loaf) for catching frames over 50ms in production.
  • web.dev/measure for synthetic benchmarks and a sanity check.

What This Means for Your Site

If your site is below 200ms INP at p75, you can stop reading. If you are above, the patterns above are probably what is wrong. The order to look:

  1. Add real-user INP measurement to find which interactions are slow. Do not guess.
  2. For each slow interaction, look at the handler's main-thread work. Yield aggressively.
  3. Reduce reactive surface (fewer fine-grained bindings, larger batched updates).
  4. Eliminate layout thrash (batch reads, then writes).

If INP is hurting your search ranking or your checkout conversion, our web development team can audit the site, identify the root causes, and ship the fixes. We have done this exact work for ecommerce, SaaS, and content sites. Tell us your INP score and your URL and we will respond within 24 hours with a quote.