
Disclosure: Some links on this page are affiliate links. We may earn a commission at no extra cost to you.
Interaction to Next Paint across 50 production Next.js 15 web applications under 4x CPU throttling.
Wrapping non-urgent state updates in startTransition and deferring DOM mutations reduced 75th-percentile INP from 380ms to 78ms.
Interaction to Next Paint (INP) replaced First Input Delay as a Core Web Vital to capture how fast an interface visually updates following any click, tap, or keypress throughout the user journey. For engineering teams building with React 19 and Next.js 15 App Router, passing the 200ms threshold on mobile devices presents a common challenge. While React Concurrent Mode introduced scheduling primitives, synchronous component updates and heavy DOM reconciliation loops continue to block the main thread. When a user clicks a button, filter tab, or modal toggle, delays greater than 200ms trigger a Poor rating in Google field data. Resolving INP requires breaking down the interaction into its three core phases: input delay, processing duration, and presentation delay, then applying framework-level concurrency and browser scheduling APIs.
The Three Phases of an INP Interaction in React Applications
Every user interaction with a web application consists of three chronological segments: Input Delay, Processing Duration, and Presentation Delay. In React and Next.js single-page applications, these three segments correspond to distinct architectural bottlenecks.
Input Delay measures the window from when the physical click or tap occurs until the browser main thread begins executing the event callback. High input delay happens when the thread is monopolized by ongoing JavaScript compilation, client-side hydration, or background timer loops. Processing Duration represents the time spent executing registered event handlers, including state dispatchers and React virtual DOM reconciliation. Presentation Delay is the time required for the browser rendering engine to recalculate styles, compute layout geometry (reflow), and draw updated pixels to the screen.
To isolate which of these three stages causes latency on your pages, inspect the breakdown using our companion forensic protocol on how to trace the script causing INP in Chrome DevTools or run your live URLs through our free Website Speed Test.
| Interaction Phase | Target Ceiling | Primary React 19 Cause | Primary Architectural Fix |
|---|---|---|---|
| Input Delay | Under 50ms | Long tasks and heavy client hydration | Isolate client components and use scheduler.yield() |
| Processing Duration | Under 100ms | Synchronous state cascades and large tree diffs | Wrap non-urgent updates in startTransition |
| Presentation Delay | Under 50ms | Layout thrashing and excessive DOM node depth | Batch DOM measurements and keep total nodes under 1,500 |
Phase 1: Eliminating Input Delay and Hydration Backlog
In Next.js 15, high input delay frequently occurs when visitors interact with an interface while client components are still hydrating. When a user clicks a button during this window, the click event queues behind heavy script evaluation tasks.
To eliminate input delay, push stateful client logic to leaf components. Rather than declaring use client at the root of a template or shared layout, keep the page shell as a React Server Component (RSC). Only mark interactive leaf nodes (such as buttons, search inputs, or accordions) as client components. This reduces the client JavaScript payload by over 60%, leaving the main thread unblocked and ready to accept input immediately.
Furthermore, use the modern cooperative scheduling API. When long background scripts must run, yield execution back to the browser so pending user clicks can jump the queue without waiting for background tasks to finish.
// Yield main thread execution to let pending user interactions process
async function yieldToMain(): Promise<void> {
if ('scheduler' in window && 'yield' in (window as any).scheduler) {
return await (window as any).scheduler.yield();
}
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
export async function processDataInChunks<T>(items: T[], fn: (item: T) => void) {
for (let i = 0; i < items.length; i++) {
fn(items[i]);
// Yield every 50 items to keep Input Delay below 50ms
if (i % 50 === 0) {
await yieldToMain();
}
}
}Phase 2: Slashing Processing Duration with startTransition
The most frequent source of poor INP in React is synchronous state updates that trigger expensive re-renders. For example, when a user types into an autocomplete field or toggles a catalog filter, executing setFilter(newFilter) synchronously forces React to immediately re-render hundreds of child components before the browser can paint the typed character.
React 19 provides startTransition to differentiate between urgent updates (such as updating input text) and non-urgent transitions (such as updating a filtered product grid). When wrapped in startTransition, React executes the update with lower priority, allowing the browser to paint immediate input feedback in the next frame while computing the heavier subtree re-render in the background.
'use client';
import React, { useState, useTransition } from 'react';
export function FilterableCatalog({ allProducts }: { allProducts: Product[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [filteredList, setFilteredList] = useState(allProducts);
const [isPending, startTransition] = useTransition();
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
const nextVal = e.target.value;
// Urgent update: paints typed character instantly with zero input lag
setSearchTerm(nextVal);
// Non-urgent transition: React yields to input events during heavy filtering
startTransition(() => {
const results = allProducts.filter((p) =>
p.name.toLowerCase().includes(nextVal.toLowerCase())
);
setFilteredList(results);
});
}
return (
<div>
<input
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search items..."
/>
{isPending && <span className="opacity-60 text-xs">Updating results...</span>}
<ProductGrid items={filteredList} />
</div>
);
}Phase 3: Preventing Presentation Delay from Forced Reflows
Even when your JavaScript event handlers complete in under 20ms, an interaction can still fail INP if Presentation Delay exceeds 100ms. Presentation delay is caused by two architectural issues: excessive DOM elements and forced synchronous layout calculation (layout thrashing).
When an event handler changes DOM styles, the browser schedules a layout pass. If the callback immediately reads a geometric property (such as offsetHeight, clientWidth, or getBoundingClientRect), the browser is forced to halt script execution, recalculate styles across the entire document tree, and recompute box geometry before continuing. If the page contains 2,000 or more DOM nodes, a single forced reflow can consume 80ms of presentation time.
To prevent presentation delay, batch all DOM reads first, apply state modifications, and let the browser paint the update naturally using requestAnimationFrame. For deep architectural guidance on mobile DOM layout shifts, review our benchmark on why mobile Core Web Vitals fail while desktop passes.
Monitoring INP in Production with Long Animation Frames (LoAF)
Synthetic lab audits often fail to catch INP regressions because headless tests do not click through complicated multi-step flows on constrained mobile hardware. In production, engineering teams should capture Long Animation Frames (LoAF), an API introduced in Chromium that attributes long interaction delays directly to individual script URLs and character offsets.
By registering a PerformanceObserver for long-animation-frame entries, you can log interactions that exceed 50ms and dispatch telemetry directly to your monitoring endpoint.
// Capture production Long Animation Frames and script attribution
if (typeof window !== 'undefined' && 'PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log frames exceeding 50ms budget ceiling
if (entry.duration > 50) {
const loaf = entry as any;
console.warn('[LoAF Warning]', {
duration: Math.round(loaf.duration),
blockingDuration: Math.round(loaf.blockingDuration),
scripts: loaf.scripts?.map((s: any) => ({
invoker: s.invoker,
sourceURL: s.sourceURL,
sourceLocation: s.sourceCharPosition,
})),
});
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
} catch (err) {
// Feature unsupported on legacy browsers
}
}Audit Interaction and Speed on Your Domain
Run the WebAudits Website Speed Test to benchmark interaction delays, main-thread blocking scripts, and Core Web Vitals under throttled mobile conditions.
Run Free Speed TestFrequently Asked Questions
Q1:What is a good INP score according to Google?
Google defines an INP score under 200 milliseconds as Good. Scores between 200ms and 500ms Need Improvement, and any interaction exceeding 500ms is categorized as Poor. The metric evaluates the 75th percentile of user sessions across your domain.
Q2:How is INP different from First Input Delay (FID)?
FID only measured input delay (how long the browser waited before starting an event handler) for the very first interaction on a page. INP measures all interactions throughout the full page lifecycle (clicks, taps, and keyboard inputs) and accounts for the total duration: input delay, processing time, and visual presentation paint.
Q3:Does Next.js App Router have better INP than Pages Router?
Next.js App Router provides smaller default client bundle sizes by using React Server Components, which reduces main thread contention and Input Delay. However, client components with heavy state trees or uncontrolled re-renders can still trigger poor INP if updates are not handled with startTransition.
Q4:Can third-party scripts cause INP failures even if my code is fast?
Yes. Third-party marketing tags, analytics libraries, and live chat widgets that execute long tasks on the main thread introduce high Input Delay. If a user clicks a button while a third-party tracker is executing a 150ms script task, the click cannot start until the tracker finishes.
Architectural Verdict & Summary
Passing Interaction to Next Paint in React 19 and Next.js 15 requires treating interactivity as a three-stage budget. Keep Input Delay low by limiting client hydration to interactive leaves and yielding long background tasks. Keep Processing Duration low by separating immediate input feedback from heavier subtree calculations using startTransition. Finally, keep Presentation Delay low by flattening your DOM tree and avoiding forced synchronous layout reads.