Fixing INP: How We Tune Interaction to Next Paint in React
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗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 and 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 input delay of the first interaction, while INP measures the full response of nearly every interaction across the page's life.
Every interaction breaks into three phases: input delay, processing time, and presentation delay. At Devya we optimize the phase that actually dominates rather than tuning all three blindly.
The two highest-leverage React fixes are wrapping non-urgent state updates in startTransition and breaking long tasks with the scheduler.yield() browser API.
INP is a field metric. We measure it with the web-vitals library or PerformanceObserver, not Lighthouse, because a lab tool cannot reproduce real user interactions.
What is INP and how is it different from FID?
INP is the Core Web Vitals metric that reports the longest interaction latency a user experiences on a page, from the moment they click, tap, or press a key until the next frame the browser paints. Google promoted INP from experimental to a core metric in March 2024, retiring First Input Delay.
The difference matters. FID measured only the input delay of the very first interaction and ignored everything after the event handler started. INP measures the entire interaction — input delay plus the handler's work plus rendering — and reports the worst one across the session. A page can post a perfect FID and still feel broken, because FID never saw the slow re-render a click triggered.
What are the three phases of an interaction?
Every INP measurement decomposes into three phases, and knowing which one is long is the whole game. Input delay is the time from the user's action until the event handler begins running, usually spent waiting for the main thread to finish other work.
Processing time is how long the event handlers take to execute, including React state updates and any synchronous work they kick off. Presentation delay is the time from the end of processing until the browser paints the next frame, dominated by layout, style, and paint.
The Chrome DevTools Performance panel labels these three segments when you record an interaction. Our rule is to optimize the segment that is actually long: on one client dashboard the real problem was input delay from an unrelated analytics script, not the processing time we first suspected.
How do we fix input delay?
Input delay is almost always the main thread being busy with a long task when the user clicks. A long task is any block of JavaScript that occupies the main thread for more than 50 milliseconds. The fix is to break long tasks into smaller chunks and yield control back to the browser between them so a pending click can be handled.
The modern browser API for this is scheduler.yield(), which returns a promise that resumes after the browser has handled higher-priority work such as input. Where it is unavailable, awaiting a setTimeout of zero is the old fallback, though it resumes at a lower priority. Our single biggest win on one project was moving a third-party analytics init off the initial load with requestIdleCallback so it stopped blocking first clicks.
How do we fix processing time?
Processing time is where React lives, and the lever is telling React which updates are urgent. In React 18 and 19, startTransition marks a state update as non-urgent so React can interrupt it to keep the interaction responsive. The typical pattern is to set the input value urgently on every keystroke while wrapping the expensive filtered-list update in startTransition so it re-renders at a lower priority.
The hook useDeferredValue does the same for a value you receive rather than set. Neither makes the filtering faster — they make it interruptible, which is exactly what INP measures. For genuinely heavy computation we move it off the main thread entirely into a Web Worker.
How do we fix presentation delay?
Presentation delay is the browser's rendering work after the handler finishes, and the usual culprit is asking it to lay out and paint more of the DOM than the user can see. The CSS property content-visibility set to auto tells the browser to skip rendering off-screen elements until they scroll near the viewport, and pairing it with contain-intrinsic-size reserves height so the scrollbar stays stable.
The other frequent cause is layout thrashing: reading a layout property like offsetHeight and then writing a style in the same loop forces synchronous reflows. Batch all reads, then all writes. For very long lists, virtualization — rendering only the visible rows — beats any CSS trick.
The three phases at a glance
Input delay runs from the action to the handler start; its common cause is a long task blocking the main thread, and our first fix is scheduler.yield() plus deferring third-party scripts.
Processing time is the handler execution; its common cause is an expensive synchronous re-render, and our first fix is startTransition, useDeferredValue, or a Web Worker. Presentation delay runs from handler end to next paint; its common cause is a large DOM or layout thrash, and our first fix is content-visibility, virtualization, and batching reads and writes.
How do we measure INP in the field?
INP is a field metric, so the number that matters comes from real users, not Lighthouse. Lighthouse cannot click buttons, so its lab INP is a synthetic estimate. We capture the real one with the web-vitals library's onINP callback, sending each metric to analytics with navigator.sendBeacon.
The web-vitals attribution build reports which element and which phase caused the worst interaction, so we fix the right thing. Chrome's field data in the CrUX report and PageSpeed Insights then confirms whether the change moved the 75th-percentile score users actually experience.
FAQ
Q: What is a good INP score?
A: 200 ms or less at the 75th percentile is good, 200 to 500 ms needs improvement, and over 500 ms is poor.
Q: Is INP a Core Web Vital?
A: Yes. INP became one of the three Core Web Vitals in March 2024, replacing First Input Delay.
Q: Does startTransition make code run faster?
A: No. It marks a state update as non-urgent so React can interrupt it; the work takes the same time but stops blocking the interaction.
Q: Can we measure INP in Lighthouse?
A: Not reliably. Lighthouse runs in a lab without real interactions; use the web-vitals library or Chrome's CrUX field data for a real INP.
Q: What is scheduler.yield()?
A: A browser API that pauses a long task and resumes it after the browser handles higher-priority work like user input, keeping input delay low.
Further Reading
Full-Stack Engineering
File Uploads in the Next.js App Router: Our Field Notes on Vercel Blob, Client Uploads, and the Callback That Never Fires on localhost
A Vercel Blob client upload sends bytes straight from the browser to storage and never routes them through a Next.js function, which is why it handles files a route handler cannot. The cost is that the database row is written by a server-to-server callback named onUploadCompleted, and that callback never reaches localhost.
Full-Stack Engineering
Web Push in the Next.js App Router: How We Ship Service Workers, VAPID, and iOS-Safe Permission Flows
Web Push is a browser API that delivers a server-sent notification to a device while the site is closed. We break down the three parts a Next.js App Router project needs — a service worker at the origin root, a VAPID key pair, and a Node.js-runtime route handler — plus the iOS Home Screen rule that silently blocks everything else.