Introduction
Interaction to Next Paint (INP) replaced First Input Delay (FID) as a Core Web Vital. While FID only measured the delay before the browser began processing the *first* user interaction, INP measures the overall responsiveness of your application by calculating the latency of *all* user clicks, taps, and keyboard inputs throughout the page lifecycle.
In complex React and Next.js applications, poor INP is usually a symptom of work competing with the interaction on the main thread: a large render, expensive event handler, layout work, or a third-party script. The goal is to keep the interaction below 200ms, then verify the result with real-user data.
A practical workflow
- Reproduce the slow interaction in a production-like build.
- Record a performance trace and identify the longest task in the interaction.
- Reduce the work, move it off the critical path, or split it into interruptible chunks.
- Validate the same interaction with field data after release.
1. Deconstructing the INP Lifecycle
When a user interacts with a web component (e.g., clicking a filter or typing in an input field), INP measures three distinct phases:
- Input Delay: The time between the user action and the execution of event handlers.
- Processing Time: The time spent executing JavaScript event handlers.
- Presentation Delay: The time spent by the browser recalculating styles, running layout algorithms, and painting the new frame to the screen.
[ User Interaction ] ──> ( Input Delay ) ──> ( Processing Time ) ──> ( Presentation Delay ) ──> [ Frame Painted ]2. Yielding Long Tasks with `scheduler.yield()`
When an event handler triggers heavy computations (such as filtering large data arrays or re-indexing search trees), executing everything synchronously blocks the browser from painting visual feedback.
Instead of running long monolithic loops, break execution into chunked tasks using scheduler.yield() (or fallback to setTimeout / requestIdleCallback):
async function processDataChunks<T>(items: T[], processFn: (item: T) => void) {
for (let i = 0; i < items.length; i++) {
processFn(items[i]);
// Yield execution back to main thread every 50 items
if (i % 50 === 0 && 'scheduler' in window && 'yield' in window.scheduler) {
await (window as any).scheduler.yield();
}
}
}3. Non-Blocking State Updates with `useTransition`
In React 18+, state updates marked with startTransition are treated as non-urgent. This allows urgent inputs (like typing or button clicks) to interrupt background rendering:
import { useState, useTransition } from "react";
export function DataFilterList({ rawItems }: { rawItems: Item[] }) {
const [query, setQuery] = useState("");
const [filteredList, setFilteredList] = useState(rawItems);
const [isPending, startTransition] = useTransition();
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextQuery = e.target.value;
// Urgent update: keep the text input immediately responsive
setQuery(nextQuery);
// Non-urgent update: allow UI to paint typing before running heavy filter
startTransition(() => {
const results = heavyFilterAlgorithm(rawItems, nextQuery);
setFilteredList(results);
});
};
return (
<div>
<input value={query} onChange={handleSearchChange} placeholder="Filter items..." />
{isPending && <span className="spinner">Updating list...</span>}
<ListView items={filteredList} />
</div>
);
}4. Measure Before and After
Use Lighthouse or the Chrome Performance panel to find a likely cause, but do not treat a lab score as the final verdict. Capture INP attribution in production and group samples by route and interaction target. Also test on a mid-range mobile device: a trace that looks harmless on a developer laptop can become a long task on a phone.
5. Key Takeaways for Production Architecture
- Avoid Layout Thrashing: Never read DOM properties (like
offsetHeightorgetBoundingClientRect) immediately after writing DOM styles inside input handlers. - Profile Real User Monitoring (RUM): Use the
web-vitalsSDK to capture INP metrics in production and log attribution data (target element, event type, load state). - Defer Non-Critical Work: Move analytics and log transmission behind
requestIdleCallback()with a timeout fallback. Use a Web Worker when the computation itself—not just the network request—is expensive. - Set a budget: Treat p75 INP under 200ms as the target and investigate interactions above 500ms urgently.