INP: Interaction to Next Paint Guide (2026)
Interaction to Next Paint (INP) measures the delay between a user's click, tap, or keypress and the next visual update. In a React app, this includes the time for the event handler to run, state to update, and the component tree to re-render. A 50-millisecond INP feels snappy; a 500-millisecond INP feels sluggish. Google's threshold for "good" is 200 ms or less; INP is now a Core Web Vital and a ranking factor, replacing the older First Input Delay (FID) metric as of March 2024.
What Is INP and Why It Matters
INP captures the entire interaction lifecycle: from the moment the browser receives the input event to when the next frame paints on screen. This includes JavaScript execution, React state updates, DOM mutations, and browser paint time. Unlike FID, which measured only the first interaction, INP measures all interactions on the page, so a slow response to the 10th click is penalized equally to a slow response to the first click.
Mobile users are particularly sensitive to INP. A 300-millisecond delay on a form input (e.g., typing in a search box) feels unresponsive and causes abandonment (Chromium Blog, 2024). E-commerce sites with slow INP see a 2–5% increase in form abandonment and cart checkout failures (Web.dev case studies, 2025).
Understanding the INP Timeline
The INP timeline consists of three phases:
- Input delay (before JavaScript runs): Time from when the browser receives the input event to when your JavaScript event handler executes. On a busy main thread, this can be 50–200 ms.
- Processing time (JavaScript execution): Time for your event handler, state updates, and React re-render to complete. This often dominates the total.
- Presentation delay (after JavaScript): Time for the browser to paint the visual update on screen.
A typical breakdown:
- Input delay: 20 ms (waiting for main thread)
- Processing: 150 ms (React event handler + re-render)
- Presentation: 20 ms (paint)
- Total INP: 190 ms (just under Google's "good" threshold of 200 ms)
Measuring INP with web-vitals
Use the getINP() function from the web-vitals library to capture INP in production:
// src/index.js or src/main.tsx
import { getINP } from 'web-vitals';
getINP(metric => {
console.log('INP:', metric.value, 'ms');
// Send to analytics
fetch('/api/analytics/inp', {
method: 'POST',
body: JSON.stringify({
metric: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
url: window.location.href,
timestamp: new Date().toISOString(),
}),
keepalive: true,
});
});
Unlike LCP, which fires once, getINP measures all interactions. The library automatically picks the slowest interaction and reports that as the final INP. This means a page with 100 fast clicks and one 400-millisecond slow click will report INP of 400 ms, triggering a "poor" rating.
Debugging INP in Chrome DevTools
Open Chrome DevTools and use the Interactions track in the Performance tab:
- Open DevTools → Performance tab → Reload page.
- Click or type on the page (interact normally).
- Stop recording (red button).
- Look for "Interactions" in the timeline.
- Click each interaction to see the breakdown: input delay, processing, presentation.
The flame chart shows which JavaScript functions ran during processing. Long rectangles (yellow, red, orange) represent expensive operations (filters, computations, DOM mutations). Hover over them to see function names.
Identifying Long Tasks in React
Long tasks are JavaScript execution that blocks the main thread for 50 ms or more. A single long task delays the next interaction by the same duration. React's re-render can be a long task if the component tree is large or the render logic is expensive.
Identify long tasks using the PerformanceObserver:
// src/utils/debugLongTasks.js
export function monitorLongTasks() {
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
startTime: entry.startTime,
duration: entry.duration,
attribution: entry.attribution, // which script triggered it
});
}
});
// Observe tasks longer than 50 ms (default)
observer.observe({ entryTypes: ['longtask'] });
}
// In your App component
import { useEffect } from 'react';
import { monitorLongTasks } from './utils/debugLongTasks';
export default function App() {
useEffect(() => {
monitorLongTasks();
}, []);
return (/* ... */);
}
Any entry with duration greater than 50 ms is a long task. The attribution field identifies which JavaScript (React bundle, third-party script) caused the long task.
Reducing INP in React Event Handlers
Optimize event handlers to complete in under 100 ms (leaving 100 ms for presentation delay). Here is a slow event handler and its optimized version:
// src/components/ProductFilter.jsx (SLOW - INP ~400 ms)
export default function ProductFilter({ products }) {
const [query, setQuery] = useState('');
const handleChange = (e) => {
const q = e.target.value;
setQuery(q);
// Blocking operation: filter 10,000 products synchronously
const filtered = products.filter(p =>
p.name.toLowerCase().includes(q.toLowerCase())
);
// Update state with filtered results
setResults(filtered);
};
return (
<input
value={query}
onChange={handleChange}
placeholder="Search products..."
/>
);
}
This blocks the main thread for 100–200 ms while filtering a large array. Users see input lag.
// src/components/ProductFilter.jsx (OPTIMIZED - INP ~50 ms)
import { useState, useTransition } from 'react';
export default function ProductFilter({ products }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const q = e.target.value;
// Update input immediately (fast)
setQuery(q);
// Defer expensive filtering to non-blocking task
startTransition(() => {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(q.toLowerCase())
);
setResults(filtered);
});
};
return (
<>
<input
value={query}
onChange={handleChange}
placeholder="Search products..."
/>
{isPending && <p>Filtering...</p>}
</>
);
}
Using useTransition(), the filter still runs asynchronously, but the input update is immediate (INP ~50 ms). The UI shows a loading indicator while filtering completes. React schedules the transition at a lower priority, so it does not block user interactions.
Breaking Up Long Event Handlers
If you cannot defer work, break it into smaller chunks:
// src/utils/scheduler.js
export async function yieldToMain() {
return new Promise(resolve => {
// Schedule callback after current frame paints
if (navigator.scheduling?.isInputPending?.()) {
// Input is pending; yield to the browser
setTimeout(resolve, 0);
} else {
// No pending input; use requestIdleCallback if available
if ('requestIdleCallback' in window) {
requestIdleCallback(resolve);
} else {
setTimeout(resolve, 0);
}
}
});
}
// In your event handler
async function handleLargeComputation(data) {
for (const chunk of splitIntoChunks(data)) {
processChunk(chunk);
await yieldToMain(); // Yield after each chunk
}
}
This allows the browser to process user input between chunks, keeping INP low.
Comparing INP to Old FID Metric
| Metric | Measured | Replaced? | Threshold (Good) |
|---|---|---|---|
| FID (First Input Delay) | First interaction only | Yes (March 2024) | ≤ 100 ms |
| INP (Interaction to Next Paint) | All interactions | Replaces FID | ≤ 200 ms |
INP's higher threshold (200 ms vs 100 ms for FID) reflects the reality that browser processing time has increased with modern React apps. However, the threshold is still strict—most users perceive 200+ ms as noticeable lag.
Key Takeaways
- INP measures the delay from user input to the next visual update (click to painted frame).
- Google's threshold for "good" is 200 ms or less; INP is now a ranking factor.
- React's state updates and re-renders contribute significantly to INP processing time.
- Use
useTransition()to defer expensive computations and keep input responsive. - Use PerformanceObserver to identify long tasks (>50 ms) that delay interactions.
- Break up computations into chunks and yield to the main thread between chunks.
Frequently Asked Questions
What is the difference between INP and FID?
FID measured only the first interaction; INP measures all interactions. A page with 99 fast clicks and one 400-millisecond slow click reports FID as the first click's latency but INP as 400 ms. INP is more representative of the user's full session and is now the Core Web Vital.
How do I reduce INP if my React components are complex?
Use React.memo() to prevent unnecessary re-renders, useMemo() to cache expensive computations, and useTransition() to defer non-blocking state updates. For very large lists, use virtual scrolling libraries like react-window to render only visible items.
Does server-side rendering (SSR) affect INP?
No. INP is measured entirely in the browser after the page has loaded. SSR affects LCP (by reducing initial HTML size) but not INP. However, if your SSR includes heavy JavaScript hydration, that can block interactions during the hydration window (until hydrateRoot completes).
Can I optimize INP for specific user interactions?
Yes. Use React's scheduler API and startTransition to prioritize critical interactions (form input, navigation) over non-critical ones (analytics, background data fetching). Measure INP per-interaction-type to identify which interactions are slowest.
Further Reading
- Web.dev: Optimize INP — Comprehensive guide to measuring and reducing Interaction to Next Paint.
- React's useTransition Hook — Official documentation on deferring non-blocking updates.
- Chromium Blog: INP Becomes Core Web Vital — Why INP replaced FID and the implications.
- Long Tasks API — MDN reference for identifying blocking JavaScript.