Headline: Interaction to Next Paint (INP) is the Core Web Vitals metric that measures how long your page takes to visually respond to a user interaction, and since March 2024 it replaced First Input Delay (FID). When my dashboard felt sluggish on every click, the fix was not a faster server — it was getting React off the main thread across the three phases of each interaction.
I spent the better part of a week chasing a "the app feels slow" complaint that no server metric explained. The API was fast, Lighthouse scored green in the lab, but every click on the filter panel lagged before anything changed on screen. The culprit was INP, and splitting each interaction into three phases made the fixes small and mechanical.
Key takeaways
- INP (Interaction to Next Paint) measures the latency from a user interaction to the next frame the browser paints; 200 ms or less is good, over 500 ms is poor, both at the 75th percentile.
- INP replaced First Input Delay (FID) as a Core Web Vital in March 2024 — FID measured only the first interaction's input delay; INP measures the full response of nearly every interaction.
- Every interaction has three phases: input delay, processing time, presentation delay. Fix the one that dominates.
-
The two highest-leverage React fixes are
startTransitionfor non-urgent updates andscheduler.yield()for long tasks. -
INP is a field metric — measure it with the
web-vitalslibrary, not Lighthouse.
What is INP and how is it different from FID?
INP reports the longest interaction latency a user experiences on a page, from click, tap, or keypress until the next painted frame. Google promoted INP to a core metric in March 2024, retiring FID. FID measured only the input delay of the first interaction and ignored everything after the handler started. INP measures the whole interaction — input delay plus handler work plus rendering — and reports the worst one across the session. A page can post a perfect FID and still feel broken.
What are the three phases of an interaction?
- Input delay: action until the event handler starts, usually waiting on a busy main thread.
- Processing time: how long your handlers run, including React state updates.
- Presentation delay: end of processing until the next paint, dominated by layout and paint.
Record an interaction in the Chrome DevTools Performance panel and it labels all three. Optimize the one that is actually long.
How do I fix input delay?
Input delay is almost always a long task — any JavaScript block occupying the main thread for over 50 ms — blocking the click. Break long tasks into chunks and yield between them with scheduler.yield():
async function processRows(rows) {
for (const chunk of chunkOf(rows, 50)) {
renderChunk(chunk);
await scheduler.yield(); // hand the main thread back so a queued click runs
}
}
It resumes after the browser handles higher-priority work like input. My biggest win was deferring a third-party analytics init with requestIdleCallback so it stopped blocking first clicks.
How do I fix processing time?
startTransition marks a React state update as non-urgent so React can interrupt it and keep the interaction responsive:
function onChange(e) {
setQuery(e.target.value); // urgent: keep the input live
startTransition(() => {
setResults(filter(items, e.target.value)); // non-urgent: interruptible
});
}
useDeferredValue does the same for a value you receive. Neither is faster — both are interruptible, which is what INP measures. For heavy computation, move it to a Web Worker.
How do I fix presentation delay?
Presentation delay is rendering work on too much DOM. content-visibility: auto skips off-screen elements until they scroll near the viewport:
.list-row {
content-visibility: auto;
contain-intrinsic-size: auto 48px; /* reserve height so the scrollbar stays stable */
}
The other cause is layout thrashing — reading offsetHeight then writing a style in the same loop. Batch reads, then writes. For long lists, virtualization beats any CSS trick.
How do I measure INP in the field?
INP is a field metric, so Lighthouse's lab number is only an estimate — it cannot click your buttons. Capture the real one with web-vitals:
import { onINP } from 'web-vitals';
onINP((metric) => {
navigator.sendBeacon('/vitals', JSON.stringify(metric));
});
The attribution build names the slow element and phase, and Chrome's CrUX field data confirms whether the 75th-percentile score actually moved.
FAQ
Q: What is a good INP score?
A: 200 ms or less at the 75th percentile is good, 200–500 ms needs improvement, over 500 ms is poor.
Q: Is INP a Core Web Vital?
A: Yes — it replaced First Input Delay in March 2024.
Q: Does startTransition make my code faster?
A: No. It marks an update non-urgent so React can interrupt it; the work takes the same time but stops blocking the interaction.
Q: Can I measure INP in Lighthouse?
A: Not reliably. Use the web-vitals library or Chrome's CrUX field data.
Q: What is scheduler.yield()?
A: A browser API that pauses a long task and resumes it after higher-priority work like input, keeping input delay low.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.
Top comments (2)
INP work is useful because it forces performance discussions back to user timing instead of bundle pride. The tricky part in React apps is that the slow interaction is often not one big mistake; it is a chain of small state updates, layout reads, and work that should have been deferred. Field notes are the right format for that.
Great insights on INP! I'm