INP vs FID: Why Google Killed First Input Delay (2026 Guide)

INP vs FID explained: why Google replaced First Input Delay with Interaction to Next Paint. Understand the full interaction lifecycle and migrate your monitoring.

BugViso

14 min read

In March 2024, Google officially deprecated First Input Delay (FID) and replaced it with Interaction to Next Paint (INP) as the responsiveness metric in Core Web Vitals. This wasn't a minor label change — it was a fundamental redefinition of what "responsive" means. Sites that passed FID with flying colors suddenly discovered they were failing INP, because FID measured only the first click's input delay while INP measures every interaction's full lifecycle throughout the entire page session.

The implications are significant: FID declared 93% of origins as "Good" in the Chrome UX Report, creating a false sense of universal responsiveness. INP, applied to the same data, reclassified roughly 30% of those origins as failing — meaning nearly a third of websites thought they were responsive but were actually delivering laggy interactions that users felt on every click, scroll, and keystroke.

This guide explains the technical mechanics of both metrics, why FID's design was structurally flawed, exactly what INP measures differently, and what you need to change in your performance monitoring stack.


What FID Measured (And Why It Was Blind to Real Problems)

First Input Delay measured one specific thing: the time between a user's first discrete input event (click, tap, or keypress) and the moment the browser's main thread became available to begin processing that event's handler.

Diagram
FID Measurement Window:

User clicks button
     │
     ├─── Main thread busy with JS execution ───┤
     │                                           │
     │◄──────── FID (input delay only) ─────────►│
     │                                           │
     │                                    Handler starts executing
     │                                           │
     │                                           ├── Processing time ──┤
     │                                           │                     │
     │                                           │              Next frame painted
     │                                           │                     │
     │◄──────────────── Total interaction latency ────────────────────►│
     │                  (FID did NOT measure this)                     │

FID's Three Structural Flaws

Flaw 1: FID only measured the first interaction.

A user who lands on a page while the main thread is idle (common — most pages complete JS execution before the first click) would register a FID of 0–10ms. But their second, third, and tenth interactions — selecting product variants, opening dropdown menus, typing in search boxes — might each experience 300ms+ delays. FID would never capture these.

Flaw 2: FID only measured the input delay phase.

The total time a user waits after clicking a button has three components:

PhaseWhat HappensMeasured by FID?Measured by INP?
Input DelayMain thread is busy; user input waits in queue✅ Yes✅ Yes
Processing TimeEvent handler executes (your onClick code runs)❌ No✅ Yes
Presentation DelayBrowser recalculates layout, paints, composites the next frame❌ No✅ Yes

A button click with 5ms input delay (FID: 5ms, "Good") but 400ms processing time and 100ms presentation delay would feel completely unresponsive to the user — yet FID would report it as perfect.

Flaw 3: FID could only be measured in the field.

FID required real user interaction, which meant it could only be captured via Real User Monitoring (RUM) or the Chrome UX Report. Lab tools like Lighthouse couldn't measure it — they used Total Blocking Time (TBT) as a proxy, creating confusion between lab and field metrics.

💡 Engineering Rule of Thumb: FID asked "how long did the first click wait to start processing?" INP asks "how long did the user wait from click to seeing the result, on every interaction?" The difference is between measuring the queue time at a restaurant versus measuring the total time from ordering to receiving your food.


What INP Measures: The Full Interaction Lifecycle

Interaction to Next Paint captures the complete user-perceived latency for every discrete interaction during the entire page session, then reports the worst (p98) interaction as the page's INP score.

typescript
// The three phases INP measures for every interaction
interface InteractionTiming {
  // Phase 1: Input Delay
  // Time between user input and handler execution start
  inputDelay: number     // Main thread was busy with other work

  // Phase 2: Processing Time
  // Time to execute all event handlers for this interaction
  processingTime: number // Your onClick/onChange/onKeyDown code

  // Phase 3: Presentation Delay
  // Time between handler completion and next frame paint
  presentationDelay: number // Style recalc + layout + paint + composite

  // Total INP = inputDelay + processingTime + presentationDelay
  total: number
}

INP Scoring Thresholds

INP ScoreRatingUser Perception
≤ 200ms🟢 GoodInteraction feels instant
201–500ms🟡 Needs ImprovementNoticeable delay, user may retry
> 500ms🔴 PoorInteraction feels broken

How INP Selects the Reported Value

INP doesn't report the single worst interaction — it reports the 98th percentile (approximate) interaction. For a page session with 50+ interactions, this is the second-worst interaction. For sessions with fewer interactions, it's the worst.

text
Example session with 20 interactions:
Interaction  1:  45ms  ✅ Good
Interaction  2:  62ms  ✅ Good
Interaction  3: 312ms  🔴 Poor (expanding accordion with heavy re-render)
Interaction  4:  78ms  ✅ Good
...
Interaction 19:  55ms  ✅ Good
Interaction 20: 198ms  ✅ Good

Reported INP: 312ms (worst interaction = p98 for <50 interactions)

Under FID: 45ms (only first interaction counted → declared "Good")
Under INP: 312ms (worst interaction counted → declared "Poor")

The Reclassification Impact: Which Sites Failed When INP Replaced FID

When Google switched from FID to INP, the percentage of origins passing Core Web Vitals responsiveness dropped dramatically:

Metric% Origins Rated "Good" (CrUX, mobile)Gap
FID93.2%—
INP64.7%28.5% of origins reclassified as failing

The Most Affected Site Categories

Site CategoryFID Pass RateINP Pass RateDrop
Static content sites (blogs, news)97%85%-12%
E-commerce (product pages)91%58%-33%
SaaS dashboards88%42%-46%
Single-page applications (SPAs)85%38%-47%
Media-heavy sites (video, interactive)82%35%-47%

SPA frameworks (React, Vue, Angular) were hit hardest because their virtual DOM reconciliation and state management produce heavy processing time on every interaction — a cost FID never captured.


Migrating Your Monitoring: FID → INP

Step 1: Update Your web-vitals Library

typescript
// ❌ Old: FID monitoring (deprecated)
import { onFID } from 'web-vitals'

onFID((metric) => {
  console.log('FID:', metric.value)
  sendToAnalytics({ name: 'FID', value: metric.value, id: metric.id })
})
typescript
// ✅ New: INP monitoring (current Core Web Vital)
import { onINP } from 'web-vitals'

onINP((metric) => {
  console.log('INP:', metric.value)

  // Access the INP attribution for debugging
  const attribution = metric.attribution
  if (attribution) {
    console.log('Event type:', attribution.interactionType)
    console.log('Input delay:', attribution.inputDelay)
    console.log('Processing time:', attribution.processingDuration)
    console.log('Presentation delay:', attribution.presentationDelay)
    console.log('Target element:', attribution.interactionTarget)
  }

  sendToAnalytics({
    name: 'INP',
    value: metric.value,
    id: metric.id,
    attribution: attribution
  })
})

Step 2: Update Your Performance Budgets

json
// lighthouse-budget.json — update responsiveness budgets
{
  "budgets": [{
    "timings": [
      {
        "metric": "interactive",
        "budget": 3500
      },
      {
        "metric": "total-blocking-time",
        "budget": 300
      }
    ]
  }]
}
text
// Performance budget translation:
FID budget:  ≤ 100ms (most sites passed trivially)
INP budget:  ≤ 200ms (requires active optimization)

TBT (lab proxy) correlation:
  TBT < 200ms   → INP likely "Good"
  TBT 200-600ms → INP likely "Needs Improvement"
  TBT > 600ms   → INP likely "Poor"

Step 3: Update Your Dashboards and Alerts

typescript
// Custom RUM beacon with INP breakdown
function reportINPBreakdown(metric: any) {
  const { attribution } = metric

  fetch('/api/rum', {
    method: 'POST',
    body: JSON.stringify({
      metric: 'INP',
      value: metric.value,
      rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
      page: location.pathname,
      // INP-specific breakdown (not available with FID)
      inputDelay: attribution?.inputDelay,
      processingTime: attribution?.processingDuration,
      presentationDelay: attribution?.presentationDelay,
      interactionType: attribution?.interactionType, // 'pointer' | 'keyboard'
      target: attribution?.interactionTarget, // CSS selector of clicked element
      timestamp: Date.now()
    }),
    keepalive: true
  })
}

Why Each INP Phase Matters for Debugging

Input Delay: Third-Party Scripts and Long Tasks

High input delay means the main thread was busy when the user interacted. The most common cause: third-party scripts (analytics, chat widgets, A/B testing) executing Long Tasks that block the event queue.

Debug approach:

typescript
// Identify what's blocking the main thread during input delay
const longTaskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn(`Long Task: ${entry.duration}ms`, entry.attribution)
    }
  }
})
longTaskObserver.observe({ type: 'longtask', buffered: true })

Processing Time: Heavy Event Handlers

High processing time means your event handler code (React re-renders, state management, API calls) is taking too long to execute.

Debug approach:

typescript
// ❌ Heavy processing: synchronous data transformation in click handler
function handleFilterChange(filter: string) {
  // Filters 10,000 products synchronously → 200ms processing time
  const filtered = allProducts.filter(p =>
    p.category === filter && p.price >= minPrice && p.inStock
  )
  setFilteredProducts(filtered)
  updateURL(filter)
  trackEvent('filter_change', filter)
}

// ✅ Fixed: defer non-critical work with scheduler.yield()
async function handleFilterChange(filter: string) {
  // Critical: update visible UI immediately
  setActiveFilter(filter)
  setFilteredProducts(allProducts.filter(p => p.category === filter))

  // Yield to the browser → allows next frame to paint
  if ('scheduler' in globalThis) {
    await scheduler.yield()
  }

  // Non-critical: run after paint
  updateURL(filter)
  trackEvent('filter_change', filter)
}

Presentation Delay: Layout Thrashing and Paint Complexity

High presentation delay means the browser is spending too long recalculating styles, layout, and painting after your handler runs.

Debug approach:

typescript
// ❌ Layout thrashing: read/write cycle forces synchronous reflow
function updateMultipleElements(elements: HTMLElement[], newHeight: number) {
  elements.forEach(el => {
    const currentHeight = el.offsetHeight  // Forces layout read
    el.style.height = `${newHeight}px`     // Forces layout write
    // Next iteration: read triggers synchronous reflow because of prior write
  })
}

// ✅ Fixed: batch reads, then batch writes
function updateMultipleElements(elements: HTMLElement[], newHeight: number) {
  // Read phase (one layout calculation)
  const heights = elements.map(el => el.offsetHeight)

  // Write phase (batched, one reflow at end)
  elements.forEach(el => {
    el.style.height = `${newHeight}px`
  })
}

INP Optimization Strategies That FID Never Required

Because FID ignored processing time and presentation delay, many optimization techniques are new to the INP era:

StrategyPhase TargetedFID ImpactINP Impact
Defer third-party scriptsInput Delay✅ Helped FID too✅ Critical
scheduler.yield() in handlersProcessing Time❌ FID didn't measure✅ Critical
requestAnimationFrame batchingPresentation Delay❌ FID didn't measure✅ Important
React useTransitionProcessing Time❌ FID didn't measure✅ Critical for React
CSS contain: layout on dynamic elementsPresentation Delay❌ FID didn't measure✅ Important
content-visibility: auto on long listsPresentation Delay❌ FID didn't measure✅ Important
typescript
// React useTransition — mark non-urgent state updates as transitions
import { useTransition, useState } from 'react'

function ProductFilter({ products }: { products: Product[] }) {
  const [isPending, startTransition] = useTransition()
  const [filtered, setFiltered] = useState(products)

  function handleFilter(category: string) {
    // Urgent: update the active filter button immediately
    setActiveCategory(category)

    // Non-urgent: filter the product list without blocking interactions
    startTransition(() => {
      setFiltered(products.filter(p => p.category === category))
    })
  }

  return (
    <div style={{ opacity: isPending ? 0.7 : 1 }}>
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  )
}

How BugViso Measures INP and Identifies the Worst Offenders

Monitoring INP requires capturing interaction metrics across real page sessions with JavaScript-driven interactions — something that automated crawlers with no user interaction cannot fully replicate for INP (since INP requires actual user inputs). However, the underlying main-thread bottlenecks that cause poor INP are measurable.

BugViso's audit engine captures the foundational metrics:

  • Total Blocking Time (TBT) measurement via PerformanceObserver Long Task detection on every crawled page, identifying pages with the highest main-thread blocking — the primary predictor of poor INP scores in the field.
  • Core Web Vitals capture including LCP, CLS, FCP, and TTFB using the official web-vitals library, establishing the complete performance baseline for every crawled page.
  • Code coverage analysis via CDP precise JavaScript coverage tracking, quantifying unused bytes in each script — the dead code that inflates parse/compile time and increases input delay.
  • Network throttling simulation under Slow 3G and Fast 3G profiles with CPU throttling, revealing how INP-related metrics degrade on the mid-range mobile devices that dominate the CrUX p75 dataset.
  • Console error and exception capture that identifies JavaScript runtime errors from event handlers — broken interaction handlers that produce infinite INP (the interaction never completes).

Frequently Asked Questions

Can INP be measured in lab tools like Lighthouse?

Not directly. INP requires real user interactions, which Lighthouse (an automated tool) doesn't produce. Lighthouse uses Total Blocking Time (TBT) as a lab proxy for INP. There is a strong correlation: pages with TBT under 200ms typically have "Good" INP scores, while TBT above 600ms correlates with "Poor" INP. For precise INP measurement, use Real User Monitoring (RUM) or the Chrome UX Report.

My FID was always under 30ms. Will my INP also pass?

Not necessarily. A low FID only means the main thread was idle when the user first interacted. If subsequent interactions trigger heavy React re-renders, complex filtering operations, or layout-thrashing animations, INP will capture those delays — even if FID was perfect. Audit every interactive element, not just the initial page load.

Does INP affect ranking differently than FID did?

Google uses the same integration point for INP as it did for FID — it's one of the three Core Web Vitals that contribute to the Page Experience ranking signal. The difference is that more pages now fail, so the competitive impact is larger. Sites that optimize INP gain an advantage over the ~30% of origins that were reclassified from passing (under FID) to failing (under INP).

What interactions does INP measure?

INP measures discrete interactions: clicks, taps, and keyboard inputs (keydown/keyup). It does not measure continuous interactions like scrolling, pinch-zoom, or drag gestures — these are handled by the compositor thread and don't block the main thread. Hover events are also excluded.

How do I find my worst INP interaction?

Use the web-vitals library with attribution enabled. The metric.attribution.interactionTarget property returns the CSS selector of the element the user interacted with when the worst INP was recorded. In production RUM, log this selector alongside the INP value to identify which specific button, input, or link is causing the highest latency. A free BugViso audit captures TBT and Long Task counts as the lab-measurable foundation for diagnosing INP bottlenecks.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.