Home›Core Web Vitals›INP

INP: Understanding Interaction to Next Paint

Carlos ReyesSeptember 19, 202613 min read

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."

Input Delay Main thread busy? Wait for current task to yield before running handler Processing Time Execute event handlers: pointerdown → pointerup → click DOM mutations happen here Presentation Delay Style recalculation Layout computation Paint + Composite = Next frame visible User taps/clicks ▸ ▸ Visual feedback Total INP = Input Delay + Processing Time + Presentation Delay ≤ 200ms

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:

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:

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

Vue

Vanilla JavaScript

Key Takeaways