Interaction to Next Paint (INP)

The metric that measures overall page responsiveness to user interactions.

Check Your Site's Interaction to Next Paint

Enter your website URL on the homepage to see your real INP performance data.

What is INP?

Interaction to Next Paint (INP) measures the latency of all user interactions during the entire page lifecycle. It replaced First Input Delay (FID) as a Core Web Vital in March 2024. INP provides a more comprehensive view of page responsiveness by considering all interactions, not just the first one.

INP Thresholds

≤ 200ms

Good

≤ 500ms

Needs Improvement

> 500ms

Poor

Quick Wins for INP

  1. Add loading="lazy" to offscreen images
  2. Debounce input handlers with 100-300ms delay
  3. Use content-visibility: auto for offscreen content
  4. Move heavy computations to Web Workers
  5. Reduce third-party JavaScript

How INP is Calculated

INP observes the latency of all click, tap, and keyboard interactions during a user's visit. The final INP value is the longest interaction observed, ignoring outliers.

Interaction Types Measured

  • Click/Tap - Mouse clicks and touch taps
  • Keyboard - Key presses (keydown, keyup)
  • Note: Scrolling and hovering are NOT included

Interaction Phases

1. Input DelayTime from interaction to event handler start
2. Processing TimeTime to execute event handlers
3. Presentation DelayTime to render the next frame

Common Causes of Poor INP

1. Long-Running JavaScript Tasks

JavaScript that takes more than 50ms to execute blocks the main thread, preventing the browser from responding to user input.

2. Large DOM Size

Pages with thousands of DOM nodes take longer to update and render, increasing presentation delay after interactions.

3. Heavy Event Handlers

Event handlers that perform complex calculations, API calls, or large state updates block the main thread during processing.

4. Synchronous Layout Thrashing

Reading and writing to the DOM repeatedly forces the browser to recalculate styles and layout multiple times.

How to Improve INP

1. Break Up Long Tasks

  • Use setTimeout or requestIdleCallback to yield
  • Split work into smaller chunks (< 50ms each)
  • Use Web Workers for heavy computations

Example: Yielding to the main thread

function yieldToMain() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function processItems(items) {
  for (const item of items) {
    processItem(item);
    await yieldToMain(); // Let browser handle events
  }
}

2. Optimize Event Handlers

  • Debounce or throttle frequent events
  • Move heavy work out of event handlers
  • Use requestAnimationFrame for visual updates

Example: Debouncing input

function debounce(fn, delay) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}

input.addEventListener('input', debounce(handleSearch, 300));

3. Reduce DOM Complexity

  • Keep DOM nodes under 1,500 elements
  • Use virtualization for long lists
  • Avoid deeply nested DOM structures
  • Use CSS containment where possible

4. Avoid Layout Thrashing

  • Batch DOM reads and writes separately
  • Use transform instead of top/left for animations
  • Cache layout values if reading repeatedly

Bad vs Good: Avoiding forced reflow

// Bad: Forces layout on every iteration
boxes.forEach(box => {
  box.style.width = box.offsetWidth + 10 + 'px';
});
// Good: Batch reads, then batch writes
const widths = boxes.map(box => box.offsetWidth);
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px';
});

Measuring INP

Using the web-vitals library

import { onINP } from 'web-vitals';

onINP((metric) => {
  console.log('INP:', metric.value);

  // Get attribution data for debugging
  const attribution = metric.attribution;
  console.log('Event type:', attribution.interactionType);
  console.log('Target:', attribution.interactionTarget);

  // Send to analytics
  analytics.track('INP', {
    value: metric.value,
    rating: metric.rating,
    interactionType: attribution.interactionType,
  });
});