Interaction to Next Paint: The Core Web Vital That Replaced FID

Performance

First Input Delay (FID) had a flaw: it only measured the delay before the browser started processing the first interaction, and it measured only the first interaction ever. FID scores looked good on pages that had a slow click just once — because by definition the first click was the only one it measured.

Interaction to Next Paint (INP) was designed to fix this. It measures every eligible interaction on the page, and reports the worst one (with a small outlier exclusion). It also measures the full interaction duration: from input to the next frame paint.

A good INP is under 200ms. Over 500ms is poor. INP became a Core Web Vital in March 2024, and pages with high INP will be penalized in Google's search ranking.

The INP Measurement

An INP-eligible interaction is a click, tap, or keyboard press. Not all interactions count: scroll, hover, and focus are excluded because users do not expect immediate visual feedback from them.

For each eligible interaction, INP measures three phases:

  1. Input delay: The time between the event firing and the browser starting to process the event handler. Usually caused by other JavaScript blocking the main thread.
  2. Processing time: The time spent running all event handlers associated with the interaction.
  3. Presentation delay: The time between event handlers completing and the next frame being painted or committed. Usually caused by style/layout work triggered by the handler.
User taps button
  [Input delay] ← JS blocking main thread
  Event handlers run
  [Presentation delay] ← layout thrashing, style recalc
  Frame paints
  INP = total time from tap to paint

Why INP Is Hard to Measure in Development

INP is measured during a real user session. In development, the page is idle, so main thread blocking from third-party scripts, analytics, and other activity is absent. DevTools Lighthouse can approximate INP, but field data from real users (via CrUX or the web-vitals library) is the authoritative source.

import { onINP } from 'web-vitals'

onINP(({ value, attribution }) => {
  const { eventType, eventTarget, interactionType } = attribution
  console.log(`INP: ${value}ms on ${eventType} at ${eventTarget}`)
})

The attribution object identifies which element was interacted with and which phase was the longest — essential for debugging.

Reducing Input Delay

Input delay is caused by a long task running on the main thread when the user interacts. Solutions:

Break up long tasks with scheduler.yield():

async function handleClick() {
  processFirstChunk()
  await scheduler.yield() // yield to browser, allow input processing
  processSecondChunk()
}

scheduler.yield() is a new API that gives the browser an opportunity to handle pending input between chunks of work. Older fallback: await new Promise(resolve => setTimeout(resolve, 0)).

Defer non-critical initialization:

// ❌ All boot code runs synchronously after DOM parse
initAnalytics()
initChatWidget()
loadPersonalization()

// ✅ Defer non-critical work
requestIdleCallback(() => {
  initAnalytics()
  initChatWidget()
  loadPersonalization()
})

Reducing Processing Time

If event handlers are doing too much synchronous work, they extend the processing phase:

// ❌ Heavy synchronous computation in handler
button.addEventListener('click', () => {
  const result = heavyComputation() // blocks for 300ms
  updateUI(result)
})

// ✅ Move heavy work off main thread
button.addEventListener('click', async () => {
  updateUI({ status: 'loading' }) // immediate feedback
  const result = await worker.compute() // off main thread
  updateUI(result)
})

Heavy computation belongs in a Web Worker. Event handlers should be fast: update immediate UI state, then delegate work.

Reducing Presentation Delay

Presentation delay is caused by layout and style work triggered by the handler:

  • Layout thrashing: Reading layout properties (like offsetHeight) immediately after writing style forces a synchronous layout calculation
  • Large DOM mutations: Inserting or removing many nodes triggers reflow
  • CSS animations triggered at interaction time: Complex animations that need to be calculated from scratch

Use content-visibility: auto on off-screen content to allow the browser to skip layout for invisible areas. Batch DOM reads and writes. Use will-change: transform sparingly on elements that animate in response to interactions.

The 200ms Budget

200ms is the threshold. For a 60fps display, a frame takes 16.67ms. For a 120Hz display, 8.33ms. The 200ms INP budget is generous — it accounts for the processing time needed for complex UI updates.

If your INP is over 200ms, identify the worst interaction using attribution.interactionType and attribution.eventTarget, then profile that specific interaction in DevTools to find which phase is the bottleneck.

Conclusion

INP is a more demanding and more honest metric than FID. It catches sluggish interactions that FID missed and measures the full chain from user input to visual response. Improving INP requires reducing main thread blocking during interactions (input delay), keeping event handlers lean (processing time), and minimizing layout work triggered by state changes (presentation delay). All three phases must be optimized together for a truly responsive page.