Complete Guide to Interaction to Next Paint (INP)

Interaction to Next Paint (INP) is the Core Web Vital that measures responsiveness. It captures how quickly your page responds to user interactions -- clicks, taps, and key presses -- by measuring the time from the user's input to when the browser paints the visual result. A good INP score is 200 milliseconds or less.

INP replaced First Input Delay (FID) as a Core Web Vital in March 2024, and it's a significantly more demanding metric. While FID only measured the delay before the first interaction's event handler ran, INP measures the complete latency of every interaction on the page and reports a value representing the worst-case experience. This means sites that passed FID with ease may fail INP, especially JavaScript-heavy single-page applications.

Key stat: According to the Chrome UX Report, only 65% of origins currently pass the INP threshold at the 75th percentile, compared to 93% that passed FID. INP is the hardest Core Web Vital for most sites to optimize.

What is Interaction to Next Paint?

INP measures the latency of user interactions with the page. An "interaction" in INP's context is a discrete user action: a click, a tap on a touchscreen, or a physical key press on a keyboard. Hover and scroll are not measured.

For each interaction, INP measures the total time from the user's physical input (e.g., mouse button down) to when the browser paints the next frame that reflects the result of that interaction. This total time has three distinct phases:

  1. Input delay: The time between the user's action and when the event handler starts executing. This delay occurs when the main thread is busy with other work (a long JavaScript task, for example) and can't respond to the input immediately.
  2. Processing time: The time it takes to execute all event handlers associated with the interaction (mousedown, mouseup, click, keydown, keyup, etc.).
  3. Presentation delay: The time from when the event handlers finish to when the browser actually paints the next frame. This includes style recalculation, layout, paint, and compositing.

INP does not simply report the worst single interaction. Instead, it uses an approximation of the 98th percentile: for pages with fewer than 50 interactions, the worst interaction is used; for pages with 50+ interactions, the algorithm ignores one interaction per 50, effectively reporting the near-worst experience while filtering out true outliers.

JavaScript
// Measure INP using the web-vitals library
import {onINP} from 'web-vitals';

onINP(({value, entries}) => {
  console.log('INP:', value, 'ms');

  // The interaction that determined INP
  const worstEntry = entries[entries.length - 1];
  console.log('INP event:', worstEntry.name);
  console.log('Input delay:', worstEntry.processingStart - worstEntry.startTime, 'ms');
  console.log('Processing time:', worstEntry.processingEnd - worstEntry.processingStart, 'ms');
  console.log('Presentation delay:', worstEntry.startTime + worstEntry.duration - worstEntry.processingEnd, 'ms');
});

INP thresholds

≤ 200ms
Good
200-500ms
Needs Improvement
> 500ms
Poor

Google evaluates INP at the 75th percentile of page visits. 200ms may sound generous, but remember that INP includes input delay, processing, and rendering -- the entire pipeline from physical input to visual output. On a page with heavy JavaScript, 200ms can be surprisingly tight.

Why INP matters for SEO and UX

1. It measures what users actually feel. When a user taps a button and nothing happens for 400ms, the page feels broken. FID could report 20ms for that same interaction because it only measured the initial input delay, not the total time to visual feedback. INP captures the complete perception of sluggishness.

2. It's the Core Web Vital most sites fail. INP has the lowest pass rate of any Core Web Vital, and the December 2025 core update increased the weight of all user experience signals. Sites with poor INP are at a measurable ranking disadvantage, especially on mobile where slower processors and limited memory amplify JavaScript performance issues.

3. It affects conversion directly. Amazon's research found that every 100ms of latency costs 1% in sales. When users feel a page is sluggish, they abandon forms, skip product explorations, and leave for faster competitors. INP is the most direct proxy for this perceived sluggishness.

Common causes of poor INP

1. Long tasks blocking the main thread

The single most common cause of poor INP is JavaScript long tasks -- any task that runs on the main thread for more than 50ms. While a long task is running, the browser cannot respond to user input. If the user clicks a button while a 300ms analytics script is executing, their click sits in a queue for up to 300ms before the event handler even starts. This input delay alone can blow past the 200ms threshold.

2. Expensive event handlers

Event handlers that do too much synchronous work: computing large state transitions, manipulating thousands of DOM nodes, running complex algorithms, or triggering cascading re-renders in a framework like React. Each of these extends the processing time phase of INP.

3. Large DOM size and complex rendering

After event handlers complete, the browser must recalculate styles, layout, and paint the result. Pages with thousands of DOM nodes, deeply nested elements, or complex CSS selectors take longer to render. A DOM with 5,000+ nodes can add 100ms+ to the presentation delay phase alone.

4. Third-party scripts

Analytics, A/B testing, chat widgets, ad scripts, and social media embeds all compete for main thread time. They often run long tasks during page lifecycle, creating input delay whenever a user happens to interact while these scripts execute. The worst offenders are scripts that use synchronous XMLHttpRequest, run heavy DOM queries, or force layout recalculation.

Step-by-step INP optimization

Identify your slowest interactions

Before optimizing, find which interactions are actually slow. Use the web-vitals library with the attribution build to log detailed breakdowns. In the field, collect INP data segmented by interaction type and page section to identify patterns. In the lab, use the Chrome DevTools Performance panel: record a session, interact with the page, and look for "Long Task" markers on the main thread that coincide with your interactions.

JavaScript
// Detailed INP debugging with attribution
import {onINP} from 'web-vitals/attribution';

onINP(({value, attribution}) => {
  console.log('INP:', value, 'ms');
  console.log('Event type:', attribution.interactionType);
  console.log('Target:', attribution.interactionTarget);

  // Breakdown of the three phases
  console.log('Input delay:', attribution.inputDelay, 'ms');
  console.log('Processing time:', attribution.processingDuration, 'ms');
  console.log('Presentation delay:', attribution.presentationDelay, 'ms');

  // Long animation frames that overlapped
  console.log('Long animation frames:',
    attribution.longAnimationFrameEntries);
});

Break up long tasks

The most impactful optimization for input delay. Break tasks longer than 50ms into smaller chunks that yield back to the main thread between each chunk. This allows the browser to process pending user input between chunks. The modern approach is scheduler.yield() (Chrome 129+), with a fallback to setTimeout(0).

JavaScript
// Modern: scheduler.yield() - yields and re-queues at same priority
async function processItems(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);

    // Yield every 5 items to let browser handle input
    if (i % 5 === 4) {
      await scheduler.yield();
    }
  }
}

// Fallback: setTimeout(0) for browsers without scheduler.yield
function yieldToMain() {
  return new Promise(resolve => setTimeout(resolve, 0));
}

async function processItemsFallback(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    if (i % 5 === 4) {
      await yieldToMain();
    }
  }
}

Optimize event handlers

Move non-visual work out of event handlers. If a click handler needs to update the UI and also send analytics, split them: update the UI synchronously (or via requestAnimationFrame) and defer the analytics to requestIdleCallback or a microtask. The visual update determines INP; the analytics work does not need to block the next paint.

JavaScript
// BAD: Everything in the handler blocks INP
button.addEventListener('click', () => {
  updateUI();           // Visual work
  sendAnalytics();      // Non-visual (50ms)
  syncToLocalStorage(); // Non-visual (30ms)
  logToConsole();       // Non-visual (10ms)
  // Total: all of these block presentation
});

// GOOD: Only visual work in the handler
button.addEventListener('click', () => {
  updateUI(); // Only the visual response

  // Defer non-visual work
  requestIdleCallback(() => {
    sendAnalytics();
    syncToLocalStorage();
    logToConsole();
  });
});

Reduce JavaScript bundle size

Less JavaScript = less parsing, less compiling, less executing = lower input delay. Code-split by route and component, tree-shake unused exports, and lazy-load non-critical modules. Every 100KB of JavaScript removed saves roughly 50ms of main thread time on a mid-range mobile device.

JavaScript
// Dynamic import for non-critical features
button.addEventListener('click', async () => {
  // Load the heavy module only when needed
  const {openModal} = await import('./modal.js');
  openModal();
});

// React: lazy load routes
const Dashboard = React.lazy(() => import('./Dashboard'));
const Settings = React.lazy(() => import('./Settings'));

Minimize rendering work

Reduce the presentation delay phase by keeping your DOM small (under 1,500 nodes), using CSS contain to limit the scope of style and layout recalculations, and avoiding forced synchronous layouts. When updating the DOM, batch your reads and writes to avoid layout thrashing.

JavaScript
// BAD: Layout thrashing - alternating reads and writes
elements.forEach(el => {
  const height = el.offsetHeight; // Read (forces layout)
  el.style.height = height + 10 + 'px'; // Write (invalidates layout)
  // Next iteration forces layout again!
});

// GOOD: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // All reads
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'; // All writes
});

Advanced INP optimization techniques

Scheduler API and prioritized tasks

The Scheduler API (scheduler.postTask()) lets you assign priority levels to tasks: user-blocking (highest), user-visible (default), and background (lowest). Visual updates after user interaction should be user-blocking; analytics and logging should be background. This ensures the browser processes input-responsive work first.

Offloading to Web Workers

Move computationally expensive work (data processing, sorting, filtering, encryption) to a Web Worker. Workers run on a separate thread and cannot block the main thread. The main thread can post a message to the worker, continue responding to user input, and receive the result asynchronously when the worker finishes.

JavaScript
// Main thread: offload heavy computation
const worker = new Worker('/sort-worker.js');

searchInput.addEventListener('input', (e) => {
  // Show loading indicator immediately (fast INP)
  showSpinner();

  // Offload expensive filtering to worker
  worker.postMessage({
    query: e.target.value,
    data: largeDataset,
  });
});

worker.addEventListener('message', (e) => {
  hideSpinner();
  renderResults(e.data.filtered);
});

React concurrent features for INP

React 18+ concurrent features are purpose-built for INP optimization. startTransition() marks state updates as non-urgent, letting React interrupt them if a higher-priority update (like user input) arrives. useDeferredValue() returns a deferred version of a value that updates at a lower priority, preventing expensive re-renders from blocking interaction responsiveness.

JSX
// React: startTransition for non-urgent updates
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleInput(e) {
    // Urgent: update the input immediately
    setQuery(e.target.value);

    // Non-urgent: React can interrupt this
    startTransition(() => {
      setResults(filterLargeDataset(e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleInput} />
      <ResultsList results={results} />
    </>
  );
}

Measuring INP: lab vs. field

INP is uniquely difficult to measure in lab environments because it depends on how users actually interact with the page. Lab tools simulate a page load but don't simulate real user click patterns, scroll behaviors, or interaction frequencies.

Field measurement is essential for INP. Use the web-vitals library to collect INP from real users and send it to your analytics platform. Segment by device type (mobile devices typically show 2-3x worse INP than desktop), browser, and page section.

Lab debugging workflow:

  1. Open Chrome DevTools Performance panel
  2. Start recording, then interact with the page as a user would
  3. Stop recording and examine the flame chart
  4. Look for long tasks on the main thread that coincide with your interactions
  5. Identify the functions consuming the most time within those tasks
  6. Use the "Interactions" track (Chrome 108+) to see INP measurements directly

Framework-specific INP fixes

React: Use startTransition and useDeferredValue for expensive state updates. Virtualize long lists with react-window or @tanstack/virtual. Memoize expensive computations with useMemo. Consider React Server Components to move rendering off the client entirely.

Next.js: Use the App Router with Server Components to minimize client-side JavaScript. For interactive components, use "use client" boundaries as narrowly as possible. The next/dynamic function with ssr: false can lazy-load heavy client components.

Vue: Use defineAsyncComponent for code splitting. Leverage v-memo directive to prevent unnecessary re-renders. For large lists, use virtual scrolling libraries. Vue 3's reactivity system is generally efficient, but computed properties that depend on large arrays can create expensive re-evaluations.

WordPress: The biggest INP challenge is third-party plugin JavaScript. Audit plugins with the Performance panel and defer non-critical scripts. Use the wp_enqueue_script with strategy => 'defer' (WordPress 6.3+). Consider Partytown to run third-party scripts in a Web Worker.

Frequently asked questions

A good INP score is 200 milliseconds or less. Scores between 200ms and 500ms need improvement, and anything above 500ms is considered poor. Google evaluates INP at the 75th percentile of page visits -- meaning 75% of your real-user visits need INP at or below 200ms for a "Good" rating.

INP replaced FID as a Core Web Vital in March 2024. FID only measured the delay before the first interaction's event handler ran. INP measures the complete latency of all interactions and reports the near-worst experience. It's a much more comprehensive and demanding metric -- sites that easily passed FID often fail INP.

INP measures clicks, taps, and keyboard presses (keydown, keyup). It does not measure hover, scroll, or pinch-to-zoom because these are continuous interactions handled differently by the browser. For each discrete interaction, INP measures the total time from user input to the browser painting the next frame that reflects the result.

INP has three phases: (1) Input delay -- time from user action to event handler start, caused by main thread being busy. (2) Processing time -- time to run all event handlers for the interaction. (3) Presentation delay -- time for the browser to render the next frame after handlers complete. Optimizing each phase requires different strategies.

Key strategies: (1) Use startTransition() for non-urgent state updates so React can interrupt them for user input. (2) Use useDeferredValue() for expensive derived values. (3) Code-split with React.lazy() to reduce initial JS. (4) Virtualize long lists with @tanstack/virtual. (5) Memoize expensive computations. (6) Use React Server Components to move rendering off the client.

Yes, and SPAs typically have the worst INP scores. In a traditional multi-page site, each navigation resets the INP measurement. In an SPA, all interactions throughout the entire session contribute to a single INP score. The more JavaScript-heavy and interactive your SPA is, the harder it is to maintain good INP, which is why frameworks are investing heavily in server components and partial hydration.

Every kilobyte of JavaScript must be downloaded, parsed, compiled, and executed -- blocking the main thread from processing user interactions. Sites with initial bundles over 500KB have a 3x higher rate of poor INP. Route-based code splitting, tree shaking, and dynamic imports reduce the initial bundle. See our JavaScript bundle optimization guide.

scheduler.yield() is a browser API (Chrome 129+) that lets JavaScript voluntarily pause execution and give the browser a chance to process pending user interactions. By yielding every 5-10ms during long computations, you prevent the main thread from being blocked for extended periods. This directly reduces INP by ensuring the browser can respond to clicks and keypresses between work chunks.

Step-by-step optimization guides for every major framework and platform:

More resources