Interaction to Next Paint (INP) is the Core Web Vital for responsiveness. It replaced First Input Delay, which only looked at the first interaction and only at the delay before work started. INP looks at the whole visit and at the whole interaction.
I get asked about it in interviews more than any other vital, because the fixes touch React internals, the event loop and measurement all at once. This is how I explain it.
What INP measures
An interaction is a click, a tap or a key press. Its latency is the time from the user input until the browser paints the next frame that reflects it. If you click a button and nothing visible changes for 400 ms, that interaction took about 400 ms.
The reported INP is roughly the worst interaction during the page visit. On pages with many interactions, a few outliers are ignored, so one freak slow click does not define the score. The point is that INP reflects what a user felt at its worst, not an average.
Try it yourself. Drag the slider to change how much work the click handler does, then click and read the measured time and its rating.
Interaction timer
Try this: drag to 400 ms, then click. The page freezes for that long (600 ms at most) and the timer measures click to next paint.
Nothing measured yet. Click the button to see your first result here.
Good is 200 ms or less, poor is above 500 ms. This is an estimate from the click to just after the next frame. The first click can read a little high while the browser warms up. A real INP is the worst of many interactions, not one click.
The three parts of an interaction
Every interaction has three phases. Input delay is the wait before your event handlers can start, usually because the main thread is busy with something else. Processing time is the time your handlers run. Presentation delay is the time from the end of the handlers until the browser has rendered and painted the next frame.
- Input delayMain thread busy
- Processing timeHandlers and renders
- Presentation delayStyle, layout, paint
- Next paintUser sees the result
Processing time is usually where React code hurts: handlers, state updates and the render they trigger.
Knowing which phase is slow tells you which fix to reach for. A long input delay means something else is hogging the thread. Long processing means your handler or render is too heavy. Long presentation delay often means a big DOM or expensive layout.
Thresholds
The official bands are measured at the 75th percentile of page loads, split by mobile and desktop. That means 75 percent of your real visits need to be at or under the limit for the page to pass.
- Good200 ms
- Needs improvement500 ms
These are the official thresholds, assessed at the 75th percentile of field data. Good is 200 ms or less, needs improvement is above 200 and up to 500 ms, and anything above 500 ms is poor.
Why the main thread is the problem
JavaScript runs on one main thread, and the same thread handles input and painting. Any task that runs longer than 50 ms counts as a long task. While it runs, the browser cannot respond to a click or paint a new frame.
So a slow INP is almost always a long task in disguise. Either one was already running when the user clicked (input delay), or your own handler started one (processing time). The fixes below all do the same thing: make tasks shorter, or move them out of the way of the next paint.
Measure first
Do not guess. The web-vitals library gives you the same INP number Chrome reports, and its attribution build tells you which element and phase caused it. Send it to your analytics so you see real users, not just your fast laptop.
import { onINP } from "web-vitals/attribution";
onINP((metric) => {
navigator.sendBeacon(
"/analytics",
JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
target: metric.attribution.interactionTarget,
})
);
});Field data tells you what is slow for users. To find out why, reproduce it locally in the Chrome DevTools Performance panel. Record the interaction, find the long task, and read the flame chart to see which function ate the time. Throttle the CPU, because your machine is faster than most of your users' phones.
Keep urgent updates urgent in React
The classic case is a search box that filters a big list. Every keystroke updates state and re-renders thousands of rows, so typing stutters.
function Search({ items }: { items: string[] }) {
const [query, setQuery] = useState("");
const results = items.filter((i) =>
i.toLowerCase().includes(query.toLowerCase())
);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<List items={results} />
</>
);
}useDeferredValue lets the input stay urgent while the list lags behind. React renders the input first, then re-renders the list in the background, and it can interrupt that render if the user types again.
function Search({ items }: { items: string[] }) {
const [query, setQuery] = useState("");
const deferred = useDeferredValue(query);
const results = useMemo(
() => items.filter((i) => i.toLowerCase().includes(deferred.toLowerCase())),
[items, deferred]
);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<List items={results} />
</>
);
}For the deferral to pay off, the slow child must be memoized (React.memo, or a useMemo on the result as above). Otherwise it re-renders with the parent on every keystroke anyway. Try the three modes below. Each row does a small amount of real busy-waiting, so the list is slow on purpose.
Deferred input
Try this: pick Blocking and type the word item fast, then repeat in the other two modes. Heads up: Blocking freezes the page for about 0.2 s per keystroke.
Each keystroke waits for the whole list to render, so typing lags. The ms figure is keystroke to next paint.
- Item 1 - alpha
- Item 2 - bravo
- Item 3 - charlie
- Item 4 - delta
- Item 5 - echo
- Item 6 - foxtrot
- Item 7 - golf
- Item 8 - hotel
- Item 9 - alpha
- Item 10 - bravo
- Item 11 - charlie
- Item 12 - delta
- Item 13 - echo
- Item 14 - foxtrot
- Item 15 - golf
- Item 16 - hotel
- Item 17 - alpha
- Item 18 - bravo
- Item 19 - charlie
- Item 20 - delta
- Item 21 - echo
- Item 22 - foxtrot
- Item 23 - golf
- Item 24 - hotel
- Item 25 - alpha
- Item 26 - bravo
- Item 27 - charlie
- Item 28 - delta
- Item 29 - echo
- Item 30 - foxtrot
- Item 31 - golf
- Item 32 - hotel
- Item 33 - alpha
- Item 34 - bravo
- Item 35 - charlie
- Item 36 - delta
- Item 37 - echo
- Item 38 - foxtrot
- Item 39 - golf
- Item 40 - hotel
- Item 41 - alpha
- Item 42 - bravo
- Item 43 - charlie
- Item 44 - delta
- Item 45 - echo
- Item 46 - foxtrot
- Item 47 - golf
- Item 48 - hotel
- Item 49 - alpha
- Item 50 - bravo
- Item 51 - charlie
- Item 52 - delta
- Item 53 - echo
- Item 54 - foxtrot
- Item 55 - golf
- Item 56 - hotel
- Item 57 - alpha
- Item 58 - bravo
- Item 59 - charlie
- Item 60 - delta
- Item 61 - echo
- Item 62 - foxtrot
- Item 63 - golf
- Item 64 - hotel
- Item 65 - alpha
- Item 66 - bravo
- Item 67 - charlie
- Item 68 - delta
- Item 69 - echo
- Item 70 - foxtrot
- Item 71 - golf
- Item 72 - hotel
- Item 73 - alpha
- Item 74 - bravo
- Item 75 - charlie
- Item 76 - delta
- Item 77 - echo
- Item 78 - foxtrot
- Item 79 - golf
- Item 80 - hotel
- Item 81 - alpha
- Item 82 - bravo
- Item 83 - charlie
- Item 84 - delta
- Item 85 - echo
- Item 86 - foxtrot
- Item 87 - golf
- Item 88 - hotel
- Item 89 - alpha
- Item 90 - bravo
- Item 91 - charlie
- Item 92 - delta
- Item 93 - echo
- Item 94 - foxtrot
- Item 95 - golf
- Item 96 - hotel
- Item 97 - alpha
- Item 98 - bravo
- Item 99 - charlie
- Item 100 - delta
- Item 101 - echo
- Item 102 - foxtrot
- Item 103 - golf
- Item 104 - hotel
- Item 105 - alpha
- Item 106 - bravo
- Item 107 - charlie
- Item 108 - delta
- Item 109 - echo
- Item 110 - foxtrot
- Item 111 - golf
- Item 112 - hotel
- Item 113 - alpha
- Item 114 - bravo
- Item 115 - charlie
- Item 116 - delta
- Item 117 - echo
- Item 118 - foxtrot
- Item 119 - golf
- Item 120 - hotel
- Item 121 - alpha
- Item 122 - bravo
- Item 123 - charlie
- Item 124 - delta
- Item 125 - echo
- Item 126 - foxtrot
- Item 127 - golf
- Item 128 - hotel
- Item 129 - alpha
- Item 130 - bravo
- Item 131 - charlie
- Item 132 - delta
- Item 133 - echo
- Item 134 - foxtrot
- Item 135 - golf
- Item 136 - hotel
- Item 137 - alpha
- Item 138 - bravo
- Item 139 - charlie
- Item 140 - delta
- Item 141 - echo
- Item 142 - foxtrot
- Item 143 - golf
- Item 144 - hotel
- Item 145 - alpha
- Item 146 - bravo
- Item 147 - charlie
- Item 148 - delta
- Item 149 - echo
- Item 150 - foxtrot
- Item 151 - golf
- Item 152 - hotel
- Item 153 - alpha
- Item 154 - bravo
- Item 155 - charlie
- Item 156 - delta
- Item 157 - echo
- Item 158 - foxtrot
- Item 159 - golf
- Item 160 - hotel
- Item 161 - alpha
- Item 162 - bravo
- Item 163 - charlie
- Item 164 - delta
- Item 165 - echo
- Item 166 - foxtrot
- Item 167 - golf
- Item 168 - hotel
- Item 169 - alpha
- Item 170 - bravo
- Item 171 - charlie
- Item 172 - delta
- Item 173 - echo
- Item 174 - foxtrot
- Item 175 - golf
- Item 176 - hotel
- Item 177 - alpha
- Item 178 - bravo
- Item 179 - charlie
- Item 180 - delta
- Item 181 - echo
- Item 182 - foxtrot
- Item 183 - golf
- Item 184 - hotel
- Item 185 - alpha
- Item 186 - bravo
- Item 187 - charlie
- Item 188 - delta
- Item 189 - echo
- Item 190 - foxtrot
- Item 191 - golf
- Item 192 - hotel
- Item 193 - alpha
- Item 194 - bravo
- Item 195 - charlie
- Item 196 - delta
- Item 197 - echo
- Item 198 - foxtrot
- Item 199 - golf
- Item 200 - hotel
The slowness is artificial: each of the 200 rows busy-waits about 1 ms (roughly 200 ms per full render, on any device). It starts after your first keystroke.
useTransition and startTransition do the same job from the other side. You wrap the state update itself and mark it as non-urgent. Use useTransition when you own the setter and want an isPending flag for a spinner. Use useDeferredValue when the value comes from props or a place you do not control.
function Tabs() {
const [tab, setTab] = useState("home");
const [isPending, startTransition] = useTransition();
function select(next: string) {
startTransition(() => setTab(next));
}
return <TabBar onSelect={select} pending={isPending} active={tab} />;
}Break up long tasks and yield
Not all slow code is a React render. A click handler that loops over a large array, parses data or runs a heavy calculation blocks everything. Split the work into chunks and give the browser a chance to handle input and paint between them.
scheduler.yield() is the purpose-built tool, and it resumes your work with higher priority than a plain timer. It is available in Chromium-based browsers but not everywhere, so check support before relying on it and keep a fallback. A setTimeout of zero works as the fallback, with the downside that your continuation goes to the back of the queue.
export function yieldToMain(): Promise<void> {
if ("scheduler" in globalThis && "yield" in (globalThis as any).scheduler) {
return (globalThis as any).scheduler.yield();
}
return new Promise((resolve) => setTimeout(resolve, 0));
}
export async function processInChunks<T>(items: T[], fn: (item: T) => void) {
let last = performance.now();
for (const item of items) {
fn(item);
if (performance.now() - last > 40) {
await yieldToMain();
last = performance.now();
}
}
}Keep event handlers thin too. Do the minimum needed to show feedback, such as updating state, and push analytics calls, logging and other side work to after the paint. Batching, debouncing and avoiding layout reads inside loops all reduce processing time without any new APIs.
Move heavy work to a Web Worker
If the work is pure computation, such as sorting, parsing or crunching a large dataset, a worker takes it off the main thread entirely. Workers cannot touch the DOM, and data is copied between threads by default, so this pays off for CPU-heavy jobs, not small ones.
// sort.worker.ts
self.onmessage = (e: MessageEvent<number[]>) => {
const sorted = [...e.data].sort((a, b) => a - b);
self.postMessage(sorted);
};
// main thread
const worker = new Worker(new URL("./sort.worker.ts", import.meta.url));
worker.onmessage = (e: MessageEvent<number[]>) => setData(e.data);
worker.postMessage(bigArray);Interview answer in 30 seconds
INP measures how long it takes from a user's click, tap or key press until the next frame is painted, and the page score is roughly the worst interaction, assessed at the 75th percentile. Good is 200 ms or less, and above 500 ms is poor. It has three parts: input delay, processing time and presentation delay. Slow INP usually comes from long tasks blocking the main thread. In React I mark non-urgent updates with startTransition or useDeferredValue, break long work into chunks that yield to the main thread, move heavy computation to a Web Worker, and keep event handlers small. I measure with web-vitals for field data and the DevTools Performance panel to find the cause.
Key takeaways
- INP is the latency from input to next paint, reported as roughly the worst interaction.
- Good is 200 ms or less, poor is above 500 ms, judged at the 75th percentile.
- Find the slow phase first: input delay, processing time or presentation delay.
useTransitionanduseDeferredValuekeep input responsive but do not speed up the work.- Yield with
scheduler.yield()where supported, with asetTimeoutfallback. - Use a Web Worker for pure CPU work, and measure in the field with
onINP.
