Fixing INP After the FID Retirement

FID measured whether your page acknowledged the first tap. INP measures whether it kept up.

First Input Delay only ever measured the delay before processing began, on a single interaction. It was easy to pass and told you very little. INP measures the full time from interaction to the next painted frame, across every interaction in the session, and reports close to the worst one. Pages that passed FID comfortably fail INP routinely.

The three parts of an interaction

  • input delay — the main thread was busy when the user tapped
  • processing time — your event handler running
  • presentation delay — the browser getting the resulting frame onto the screen

Diagnosis starts with establishing which of the three dominates, because the fixes have nothing in common. Input delay is a scheduling problem, processing time is your code, and presentation delay is usually layout cost or an oversized DOM.

Find the long tasks

// log anything blocking the main thread for over 50ms
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, Math.round(entry.duration), entry.attribution);
  }
}).observe({ type: 'longtask', buffered: true });

Run it with the CPU throttled, not on your laptop. Field data comes from real devices on real networks, and a mid-range Android is where INP is won or lost. Lab numbers on a fast machine will tell you everything is fine right up until the field data disagrees.

Yield rather than optimise

The most reliable INP fix is not making the work faster. It is breaking it up so the browser can paint between the pieces. A 200ms handler that yields four times behaves completely differently from a 180ms one that never does.

  • perform the visible update first, then yield, then do the rest
  • move anything the user is not waiting on out of the handler entirely
  • watch for listeners you did not write — a tag manager firing on click is a common cause
  • large DOM trees make every style recalculation expensive; some INP failures are just an oversized page

LCP is a different problem with a shared cause

INP is main-thread contention. LCP is usually the critical request chain — how many round trips must complete before the largest element can render. They share a root cause often enough to be worth fixing together: too much third-party work, loaded too early.

On this site the LCP fix was the request chain rather than the JavaScript. The stylesheet and both font families were being fetched from external origins before anything could paint. Self-hosting the fonts as variable files, inlining the critical CSS and preloading the fonts removed the external hops entirely, so first paint no longer waits on a DNS lookup to somebody else's server.

<!-- fonts preloaded from our own origin -->
<link rel="preload" href="/fonts/geist-var.woff2"
      as="font" type="font/woff2" crossorigin />

<!-- critical CSS inlined; no render-blocking stylesheet request -->
<style>/* ... */</style>

Two variable font files replaced four static weights, and one of the weights being requested turned out to be used nowhere on the site. Auditing what you actually load is frequently the entire optimisation.

What to measure

  • field data over lab data, always — Chrome UX Report or your own real-user monitoring
  • the 75th percentile, because that is what gets graded
  • per-interaction attribution, so you know which control is slow
  • INP after every deploy, since one new third-party tag can undo months of work in an afternoon
A passing Lighthouse score on a desktop is not evidence. INP is graded on real users, and most of them are on phones you would never choose to develop on.

From the same work

Failing Core Web Vitals on mobile?