# INP: The Metric That Replaced FID and Why You Care
By Govind Bajaj · 2025

Tags: core-web-vitals, inp, performance, web-performance, user-experience
FID died in March 2024. INP took its place. FID measured one interaction. INP measures all of them. Most sites that passed FID are failing INP.
FID died in March 2024.

Google replaced First Input Delay with Interaction to Next Paint. FID measured the delay on the first user interaction only. INP measures the responsiveness of all interactions throughout the entire page lifecycle. The change seems subtle. The impact is massive.

Sites that comfortably passed FID are now failing INP. A page with a 50ms first interaction but 800ms subsequent interactions got a Good FID score and a Poor INP score. The new metric tells the truth about user experience in a way FID never could.

## What INP Actually Measures

INP tracks the latency of every tap, click, and keyboard interaction on a page. It reports the worst interaction (or the 98th percentile for pages with many interactions). The threshold for Good is under 200 milliseconds.

The measurement has three parts:

1. Input delay: Time between user action and event handler execution (main thread might be busy)
2. Processing time: How long your event handler runs
3. Presentation delay: Time to render the next frame after your handler completes

INP is the sum of these three. Your JavaScript directly controls parts 1 and 2.

## Why Your Site Probably Fails INP

In 2025, only 77% of mobile sites pass the INP threshold. The 23% that fail share common patterns:

Long tasks block the main thread. Any JavaScript that runs for more than 50ms blocks interactions. Third-party scripts, large renders, and synchronous computations are the usual culprits.

Heavy event handlers are another cause. A click handler that filters a 1000-item list, updates React state, and re-renders the entire page can take 500ms or more. INP captures every millisecond.

Layout thrashing is the third pattern. Reading layout properties (offsetHeight, getBoundingClientRect) then immediately writing styles forces the browser to recalculate layout synchronously. Doing this in a loop is devastating.

## Measuring INP in Development

The web-vitals JavaScript library reports INP for real users:

```tsx
// app/web-vitals.ts
import { onINP } from 'web-vitals'

export function reportWebVitals() {
  onINP((metric) => {
    console.log('INP:', metric.value, 'ms')
    
    if (metric.value > 200) {
      fetch('/api/metrics', {
        method: 'POST',
        body: JSON.stringify({
          name: 'INP',
          value: metric.value,
          id: metric.id
        })
      })
    }
  }, { reportAllChanges: true })
}
```

For lab testing, Lighthouse measures Total Blocking Time (TBT), which correlates strongly with INP. A TBT under 200ms typically means good INP.

## Optimization Strategy 1: Yield to the Main Thread

Break long tasks into smaller chunks using setTimeout or scheduler.yield():

```ts
// Bad: blocks for 500ms
function filterProducts(products, query) {
  return products.filter(p => 
    p.name.toLowerCase().includes(query.toLowerCase())
  )
}

// Better: yield between chunks
async function filterProducts(products, query) {
  const batchSize = 100
  const results = []
  
  for (let i = 0; i < products.length; i += batchSize) {
    const batch = products.slice(i, i + batchSize)
    results.push(...batch.filter(p => 
      p.name.toLowerCase().includes(query.toLowerCase())
    ))
    
    if (i + batchSize < products.length) {
      await new Promise(resolve => setTimeout(resolve, 0))
    }
  }
  
  return results
}
```

For React specifically, use useDeferredValue and useTransition to mark updates as non-urgent:

```tsx
import { useDeferredValue, useState } from 'react'

function ProductList({ products }) {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  
  const handleChange = (e) => setQuery(e.target.value)
  
  const filtered = products.filter(p => 
    p.name.toLowerCase().includes(deferredQuery.toLowerCase())
  )
  
  return (
    <div>
      <input value={query} onChange={handleChange} />
      {deferredQuery !== query && <Spinner />}
      <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
    </div>
  )
}
```

## Optimization Strategy 2: Virtualize Long Lists

Rendering 1000 list items creates 1000 DOM nodes. Even if the data is static, scrolling and filtering become slow.

```tsx
import { FixedSizeList as List } from 'react-window'

function VirtualizedProductList({ products }) {
  return (
    <List
      height={600}
      itemCount={products.length}
      itemSize={80}
      itemData={products}
    >
      {({ index, style, data }) => (
        <div style={style}>
          <ProductCard product={data[index]} />
        </div>
      )}
    </List>
  )
}
```

react-window only renders visible items. A 1000-item list renders 15 DOM nodes instead of 1000. Filtering and scrolling become instant.

## Takeaways

- INP replaced FID in March 2024 and measures all interactions, not just the first one
- The INP threshold is 200ms; 23% of mobile sites currently fail
- Long tasks on the main thread are the primary cause of poor INP
- Use useDeferredValue and useTransition in React to prioritize user interactions
- Virtualize lists with react-window to reduce DOM size
- Debounce expensive computations that run in response to user input
- Monitor INP with the web-vitals library and alert when it exceeds 200ms