Dev Log · Jul 6, 2026

INP(Interaction to Next Painting)

webdev

Dev Log: Chasing INP — Why "Fast" Isn't Always "Responsive"

For years, our performance conversations started and ended with load time. LCP, bundle size, image optimization — all the usual suspects. But somewhere along the way I realized we'd been ignoring the part of the experience users actually spend most of their time in: after the page loads.

That's where INP (Interaction to Next Paint) comes in. It replaced FID as a Core Web Vital back in March 2024, and honestly, it exposed a blind spot in how we'd been thinking about performance.

The problem with FID

FID only measured the delay before the first interaction's event handler started running. It didn't care how long that handler took to execute, and it definitely didn't care about anything that happened after the first click. A page could pass FID with flying colors and still feel completely broken the moment a user tried to open a dropdown, filter a list, or submit a form.

INP fixes that by watching every click, tap, and keypress throughout the entire page visit, and reporting the worst (or near-worst) one. No more grading on a curve for the first impression only.

What actually gets measured

Every interaction breaks down into three phases, and all three count toward the final number:

  • Input delay — the gap between the user's action and the browser actually starting to run the handler. Usually caused by the main thread being busy with something else.
  • Processing time — how long your event handler itself takes to run. This is where expensive state updates, synchronous DOM reads/writes, and "helpful" analytics calls tend to live.
  • Presentation delay — the time between your handler finishing and the browser painting the next frame. Big DOM trees and layout thrashing show up here.

The threshold: 200ms or less at the 75th percentile is "good." Between 200-500ms needs work. Above 500ms is poor, full stop.

Where we were actually losing time

Profiling our own interactions in DevTools' Performance panel turned up the usual suspects:

  1. Syncing form state to a global store on every keystroke. Every character typed triggered a re-render of components that had nothing to do with the input. Classic mistake — moved it to local state, synced to the store only on blur.
  2. Undebounced handlers on high-frequency events. Scroll and rapid input handlers were firing constantly and eating main thread time that should've gone to painting.
  3. Long tasks blocking unrelated interactions. A user clicking a button while an unrelated background computation was mid-flight had to wait for that task to finish before their click even registered.

What actually helped

// Split urgent updates (input value) from non-urgent ones (expensive filtering)
const [isPending, startTransition] = useTransition();

function handleSearch(query) {
  setInputValue(query); // high priority, updates immediately
  startTransition(() => {
    setSearchResults(filterLargeDataset(query)); // low priority, interruptible
  });
}
// Defer expensive derived values without blocking the input itself
const deferredQuery = useDeferredValue(query);
const results = useMemo(
  () => expensiveFilter(list, deferredQuery),
  [list, deferredQuery]
);
// Break up long synchronous tasks so the main thread can breathe
async function processLargeArray(items) {
  for (let i = 0; i < items.length; i++) {
    doWork(items[i]);
    if (i % 100 === 0) {
      await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
    }
  }
}

On the CSS side, preferring transitions over JS-driven animations for interactive elements helped too — they run on the compositor thread instead of competing with everything else on main.

The takeaway

INP is a reminder that responsiveness isn't a one-time checkbox you tick during the initial load. It's a budget you spend — or blow — every single time a user touches your UI. A page can be fast to load and still feel sluggish the moment someone actually starts using it.

Next up: setting up proper RUM so we're not just profiling our own clicks in DevTools, but seeing what real devices in the field are actually experiencing.