INP: Understanding Interaction to Next Paint
Interaction to Next Paint (INP) is the Core Web Vital that measures a page's responsiveness to user input. Officially replacing First Input Delay (FID) in March 2024, INP represents a fundamental shift in how Google evaluates interactivity — from a single first-touch measurement to a comprehensive assessment of every interaction throughout a page's lifecycle.
INP is measured in milliseconds and reflects the latency between a user's discrete input (click, tap, or key press) and the next visual update the browser paints to the screen. The threshold is clear: an INP of 200ms or less is "Good," between 200ms and 500ms "Needs Improvement," and above 500ms is "Poor." Understanding the anatomy of an interaction — and what makes each phase slow — is the foundation for bringing INP under control. For broader context on how INP fits alongside LCP and CLS, see our Core Web Vitals overview.
How INP Selects Its Value
Unlike FID, which reported the input delay of only the first interaction, INP considers every qualifying interaction during a page visit. The final INP value is not the worst interaction, nor the average — it is the interaction at the 98th percentile. On pages with fewer than 50 interactions, the algorithm uses the second-worst interaction. On pages with 50-100 interactions, the third-worst is used, and so on.
This selection mechanism means that a page with one bad interaction out of 100 total interactions will still report a good INP — the metric is designed to forgive isolated outliers while catching persistent responsiveness problems. However, if a page has only 10 interactions, a single bad one becomes the INP value, making low-interaction pages more sensitive.
Anatomy of an Interaction
Every interaction that INP tracks passes through three sequential phases. The total duration across all three phases constitutes the interaction's latency — the number that determines whether the interaction is "good" or "poor."
Phase 1: Input Delay
Input delay is the time between the user's physical action and the browser beginning to execute the associated event handler. This delay occurs because JavaScript is single-threaded — the browser's main thread can only execute one task at a time. If a long-running task (a large JavaScript parse, a complex recalculation, or a third-party script execution) is occupying the main thread when the user taps, the browser queues the input event and waits.
Input delay is the phase most affected by third-party scripts. Analytics libraries, ad scripts, and social media widgets often execute heavy initialization code that occupies the main thread for hundreds of milliseconds. When a user interacts during one of these long tasks, the input is delayed until the task completes.
Phase 2: Processing Time
Processing time covers the execution of all event handler callbacks associated with the interaction. A single click dispatches multiple events: pointerdown, mousedown, pointerup, mouseup, click. If any of these handlers perform expensive operations — DOM manipulation, data processing, state management — the processing time grows.
Common patterns that inflate processing time:
- Synchronous DOM reads after writes: Reading layout properties (like
offsetHeight) after modifying the DOM forces the browser to perform synchronous layout (forced reflow) before returning the value. - Large state updates: Framework re-renders triggered by state changes can involve diffing large virtual DOM trees and applying extensive DOM patches.
- Synchronous data processing: Parsing, sorting, or filtering large datasets within an event handler blocks the thread for the entire computation.
Phase 3: Presentation Delay
After all event handlers complete, the browser must render the visual changes to the screen. This involves style recalculation (applying CSS rules to the changed DOM), layout computation (calculating the position and size of all affected elements), painting (rasterizing the elements), and compositing (combining paint layers into the final frame).
Presentation delay is often overlooked because developers focus on their JavaScript code (processing time) while ignoring the rendering cost of the DOM changes they trigger. A handler that adds 500 DOM nodes in a single synchronous operation has minimal processing time but creates enormous presentation delay as the browser recalculates layout for the entire subtree.
Long Tasks and Their Impact
A "long task" in browser performance terminology is any task that occupies the main thread for more than 50ms. Long tasks are the primary cause of poor INP because they block the browser from processing user input promptly.
The Long Tasks API allows you to detect these in production:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration > 50ms by definition
console.log(`Long task: ${entry.duration}ms`,
entry.attribution);
}
});
observer.observe({ type: 'longtask', buffered: true });
Integrating long task detection with your error tracking and monitoring system enables you to correlate poor INP scores with specific long tasks, identifying exactly which code paths are causing delays.
Breaking Up Long Tasks
The fundamental strategy for improving INP is breaking long tasks into smaller chunks that individually complete in under 50ms. Between chunks, the browser has an opportunity to process pending user input — a concept called "yielding to the main thread."
Yielding with scheduler.yield()
The scheduler.yield() API (supported in Chrome 115+) provides the most ergonomic yielding mechanism. Unlike setTimeout(0), which places the continuation at the back of the task queue, scheduler.yield() preserves task priority — the continuation runs before other queued tasks of equal or lower priority:
async function processLargeDataset(items) {
const CHUNK_SIZE = 100;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
processChunk(chunk);
// Yield to the main thread between chunks
if (i + CHUNK_SIZE < items.length) {
await scheduler.yield();
}
}
}
requestIdleCallback for Non-Urgent Work
For work that does not need to complete immediately — analytics processing, prefetching, or speculative rendering — requestIdleCallback schedules execution during idle periods when the main thread has no pending tasks:
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
task.execute();
}
if (tasks.length > 0) {
requestIdleCallback(processRemainingTasks);
}
});
Web Workers for Heavy Computation
Operations that are computationally intensive but do not require DOM access — data parsing, encryption, image processing, complex sorting — can be offloaded to a Web Worker. Workers run on a separate thread and communicate with the main thread via message passing, completely eliminating main-thread contention:
// Main thread
const worker = new Worker('/workers/data-processor.js');
worker.postMessage({ type: 'sort', data: largeArray });
worker.onmessage = (event) => {
renderSortedData(event.data);
};
// Worker (data-processor.js)
self.onmessage = (event) => {
const { type, data } = event.data;
if (type === 'sort') {
const sorted = data.sort(complexComparator);
self.postMessage(sorted);
}
};
Optimizing Event Handlers
Event handler optimization targets the processing time phase of INP. The goal is to minimize the work done synchronously during the handler while deferring non-essential work to after the next paint.
Debouncing and Throttling
Rapid-fire events like input, scroll, and resize can trigger handlers dozens of times per second. While INP specifically tracks discrete interactions (click, tap, keypress), excessive handler execution from continuous events consumes main-thread time and increases input delay for subsequent discrete interactions.
Avoiding Forced Reflow
Forced reflow — also called layout thrashing — occurs when JavaScript reads a layout property after modifying the DOM, forcing the browser to synchronously recalculate layout before returning the value:
// BAD: Forces reflow on each iteration
for (const item of items) {
item.style.width = container.offsetWidth + 'px';
// Reading offsetWidth after style change = forced reflow
}
// GOOD: Batch reads, then writes
const width = container.offsetWidth; // Single read
for (const item of items) {
item.style.width = width + 'px'; // Batched writes
}
Third-Party Script Impact on INP
Third-party scripts are the most common cause of poor INP on otherwise well-optimized pages. Analytics libraries, A/B testing tools, chat widgets, social media embeds, and ad networks all execute JavaScript on the main thread, competing for the same resources your event handlers need.
Strategies for managing third-party impact:
- Defer loading: Load third-party scripts after the page's critical content is interactive. Use
deferor dynamicimport()triggered by a user interaction or a timer. - Audit execution time: Use the Performance panel in DevTools to identify which third-party scripts create long tasks. Correlate these with INP data from your APM system to quantify their impact.
- Use facades: Replace heavy embeds (video players, chat widgets, maps) with lightweight placeholder images that load the full widget only when the user explicitly interacts with them.
- Isolate with iframes: Third-party scripts loaded in an iframe run in their own execution context, preventing them from blocking the main page's main thread.
Measuring INP in the Field
Lab tools like Lighthouse cannot measure INP because they perform no real user interactions. INP can only be measured through Real User Monitoring (RUM) using the web-vitals library or browser APIs.
The PerformanceObserver API with the event entry type provides granular interaction data:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.interactionId groups related events
// entry.processingStart - entry.startTime = input delay
// entry.processingEnd - entry.processingStart = processing
// entry.duration - (entry.processingEnd - entry.startTime)
// = presentation delay
if (entry.interactionId) {
console.log({
type: entry.name,
duration: entry.duration,
inputDelay: entry.processingStart - entry.startTime,
processing: entry.processingEnd - entry.processingStart
});
}
}
});
observer.observe({
type: 'event',
buffered: true,
durationThreshold: 16
});
Integrating INP measurement with distributed tracing allows you to correlate slow interactions with backend operations, revealing whether a click handler's slowness comes from a synchronous API call or from client-side processing.
Framework-Specific INP Optimization
React
- Use
React.memoanduseMemoto prevent unnecessary re-renders in response to interactions - Use
useTransitionfor state updates that can be deferred — React will yield to the main thread during low-priority updates - Avoid synchronous state updates that trigger large component tree re-renders during click handlers
Vue
- Use
v-oncefor content that never changes to eliminate reactivity overhead - Leverage Vue's built-in component lazy loading with
defineAsyncComponent - Use
shallowReffor large data structures that don't need deep reactivity
Vanilla JavaScript
- Use event delegation to reduce the number of individual event listeners
- Batch DOM mutations using
DocumentFragmentbefore appending to the live DOM - Prefer CSS class toggling over individual style property manipulation
Key Takeaways
- INP measures the latency from user input to the next visual update — target ≤ 200ms at p75
- INP reports the near-worst interaction (98th percentile) across the entire page lifecycle
- Each interaction has three phases: input delay, processing time, and presentation delay
- Long tasks (>50ms) on the main thread are the primary cause of high input delay
- Break long tasks with
scheduler.yield(),requestIdleCallback, or Web Workers - Avoid forced reflow (layout thrashing) by batching DOM reads before writes
- Third-party scripts are the most common uncontrolled cause of poor INP
- INP can only be measured in the field with RUM — lab tools cannot simulate real interactions