In March 2024, Google officially replaced First Input Delay (FID) with the new Interaction to Next Paint (INP) metric as part of Core Web Vitals. INP better reflects actual page responsiveness and is more challenging to optimize. This guide explains the differences and shows how to improve your scores.

What Was FID?

First Input Delay measured the time from the user’s first interaction (click, tap) until the browser could start handling that event. It only measured the first interaction.

FID Limitations

  1. Only first interaction - ignored all subsequent ones
  2. Only delay - didn’t measure processing time
  3. Easy to optimize - fast initial load was enough
  4. Didn’t reflect reality - page could be slow after first interaction

What is INP?

Interaction to Next Paint measures page responsiveness throughout the entire time of use. It considers all interactions (clicks, taps, key presses) and reports the worst one (technically: the 98th percentile).

What Does INP Measure?

INP measures the time from interaction to the moment the next frame is rendered:

INP = Input Delay + Processing Time + Presentation Delay
  1. Input Delay - time waiting to start processing (like FID)
  2. Processing Time - time executing event handlers
  3. Presentation Delay - time rendering DOM changes

INP Thresholds

ScoreRating
≤ 200msGood (green)
200ms - 500msNeeds Improvement (orange)
> 500msPoor (red)

FID vs INP Comparison

AspectFIDINP
What it measuresOnly delayDelay + processing + rendering
Which interactionsOnly firstAll (reports worst)
Thresholds≤100ms good≤200ms good
Optimization difficultyLowHigh
StatusDeprecated (March 2024)Current Core Web Vital

How to Measure INP?

Lab Data

Lighthouse (Chrome DevTools):

  • INP is not directly measured in Lighthouse
  • Use Total Blocking Time (TBT) as a proxy

Chrome DevTools - Performance:

  1. Open Performance panel
  2. Record while interacting with the page
  3. Look for long tasks in Main thread

Field Data (Real User Data)

web-vitals library:

import { onINP } from 'web-vitals';

onINP((metric) => {
  console.log('INP:', metric.value);
  console.log('Rating:', metric.rating); // good, needs-improvement, poor
  console.log('Entries:', metric.entries); // Interaction details
});

Google Search Console:

  • Core Web Vitals report shows INP from CrUX data

PageSpeed Insights:

  • “Diagnose performance issues” section shows INP

Chrome DevTools - Web Vitals Overlay

  1. Open DevTools
  2. Press Ctrl+Shift+P
  3. Type “Show Web Vitals”
  4. Interacting with the page will show INP in real-time

Causes of Poor INP

1. Long JavaScript Tasks

JavaScript blocks the main thread. Any task >50ms is a “long task” affecting INP.

// Bad - long synchronous task
function processData(items) {
  items.forEach(item => {
    // Heavy computations...
    heavyComputation(item);
  });
}

// Good - split into smaller tasks
async function processDataAsync(items) {
  for (const item of items) {
    heavyComputation(item);
    // Yield control to the browser
    await scheduler.yield();
  }
}

// Alternative with setTimeout
function processDataChunked(items, chunkSize = 10) {
  let index = 0;

  function processChunk() {
    const chunk = items.slice(index, index + chunkSize);
    chunk.forEach(heavyComputation);
    index += chunkSize;

    if (index < items.length) {
      setTimeout(processChunk, 0);
    }
  }

  processChunk();
}

2. Heavy Event Handlers

// Bad - heavy handler blocks rendering
button.addEventListener('click', () => {
  // 500ms of computations...
  processLargeDataset();
  updateUI();
});

// Good - separate computations from UI
button.addEventListener('click', async () => {
  // Immediately show feedback
  button.disabled = true;
  showSpinner();

  // Computations in next task
  await scheduler.yield();
  const result = processLargeDataset();

  // Update UI
  await scheduler.yield();
  updateUI(result);
  hideSpinner();
  button.disabled = false;
});

3. Too Many Re-renders

// Bad - each change triggers reflow
items.forEach(item => {
  const div = document.createElement('div');
  div.textContent = item.name;
  container.appendChild(div); // Reflow!
});

// Good - batch DOM operations
const fragment = document.createDocumentFragment();
items.forEach(item => {
  const div = document.createElement('div');
  div.textContent = item.name;
  fragment.appendChild(div);
});
container.appendChild(fragment); // Single reflow

4. Layout Thrashing

// Bad - multiple reads and writes
elements.forEach(el => {
  const height = el.offsetHeight; // Read (force layout)
  el.style.height = height + 10 + 'px'; // Write (invalidate layout)
});

// Good - read first, then write
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px';
});

5. Third-party Scripts

Third-party scripts (analytics, ads, chat widgets) often have heavy handlers.

INP Optimization Techniques

1. Scheduler API (scheduler.yield)

New API allowing you to yield control to the browser:

async function handleClick() {
  // Step 1
  await scheduler.yield();

  // Step 2
  await scheduler.yield();

  // Step 3
}

// Polyfill for older browsers
if (!('scheduler' in window)) {
  window.scheduler = {
    yield: () => new Promise(resolve => setTimeout(resolve, 0))
  };
}

2. requestIdleCallback

Execute less urgent tasks when the browser is idle:

// Background processing
function processInBackground(items) {
  let index = 0;

  function processNext(deadline) {
    while (index < items.length && deadline.timeRemaining() > 0) {
      processItem(items[index]);
      index++;
    }

    if (index < items.length) {
      requestIdleCallback(processNext);
    }
  }

  requestIdleCallback(processNext);
}

3. requestAnimationFrame

Synchronize visual changes with frame rate:

// Bad - may hit mid-frame
button.addEventListener('click', () => {
  element.style.transform = 'translateX(100px)';
});

// Good - synchronized with frame
button.addEventListener('click', () => {
  requestAnimationFrame(() => {
    element.style.transform = 'translateX(100px)';
  });
});

4. Web Workers

Move heavy computations off the main thread:

// main.js
const worker = new Worker('worker.js');

button.addEventListener('click', () => {
  showSpinner();
  worker.postMessage({ data: largeDataset });
});

worker.onmessage = (e) => {
  hideSpinner();
  displayResults(e.data);
};

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

5. Debouncing and Throttling

// Debounce - execute after sequence ends
function debounce(fn, delay) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}

// Throttle - execute at most once per X ms
function throttle(fn, limit) {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Usage
input.addEventListener('input', debounce(handleSearch, 300));
window.addEventListener('scroll', throttle(handleScroll, 100));

6. Passive Event Listeners

// Bad - may block scrolling
element.addEventListener('touchstart', handler);

// Good - declares that preventDefault won't be called
element.addEventListener('touchstart', handler, { passive: true });

7. CSS contain

Limit the area that needs to be recalculated:

.widget {
  contain: content; /* Isolates layout and paint */
}

.modal {
  contain: strict; /* Full isolation */
}

8. content-visibility

Defer rendering of elements outside the viewport:

.card {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* Estimated height */
}

Framework Optimization

React

// Use startTransition for less urgent updates
import { startTransition } from 'react';

function handleClick() {
  // Urgent update - immediate
  setIsLoading(true);

  // Less urgent - can be deferred
  startTransition(() => {
    setResults(computeExpensiveResults());
  });
}

// useDeferredValue for expensive renders
import { useDeferredValue } from 'react';

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);

  // Render with deferred query
  return <Results query={deferredQuery} />;
}

// memo to avoid unnecessary re-renders
const ExpensiveComponent = React.memo(({ data }) => {
  // ...
});

Vue

<script setup>
import { computed, shallowRef } from 'vue';

// shallowRef for large objects
const largeData = shallowRef(null);

// Lazy computed
const expensiveComputed = computed(() => {
  // Calculations only when needed
});
</script>

<template>
  <!-- v-once for static content -->
  <div v-once>{{ staticContent }}</div>

  <!-- v-memo for caching -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id]">
    {{ item.name }}
  </div>
</template>

INP Optimization Checklist

JavaScript

  • Split long tasks into smaller ones (under 50ms)
  • Use scheduler.yield() or setTimeout(0)
  • Move heavy computations to Web Workers
  • Debounce/throttle event handlers

DOM

  • Batch DOM operations (DocumentFragment)
  • Avoid layout thrashing
  • Use requestAnimationFrame for animations
  • Apply CSS contain for isolation

Third-party

  • Defer non-essential scripts
  • Lazy-load widgets (chat, social)
  • Consider server-side GTM

Framework

  • React: startTransition, useDeferredValue
  • Vue: v-once, v-memo, shallowRef
  • Virtualization for long lists

Monitoring INP

Real User Monitoring (RUM)

import { onINP } from 'web-vitals';

onINP((metric) => {
  // Send to analytics
  gtag('event', 'web_vitals', {
    metric_name: metric.name,
    metric_value: metric.value,
    metric_rating: metric.rating,
    metric_delta: metric.delta
  });

  // Or to your own endpoint
  navigator.sendBeacon('/analytics', JSON.stringify({
    name: 'INP',
    value: metric.value,
    page: location.pathname
  }));
});

Alerts for Poor INP

Set up alerts in Google Search Console or your own monitoring system when INP exceeds the threshold.

Summary

INP is a more challenging metric than FID, but it better reflects actual user experience. Key points:

  1. INP measures the entire interaction - not just delay
  2. Reports the worst interaction - you need to optimize everything
  3. Split long tasks - no task should take >50ms
  4. Yield control to the browser - scheduler.yield(), requestIdleCallback
  5. Web Workers - for heavy computations
  6. Monitor with RUM - lab data isn’t enough

The goal is INP below 200ms for 75% of users. To understand how INP fits alongside other ranking factors like rendering, structured data, and E-E-A-T, check out our ultimate guide to web technologies and SEO.

Sources

  1. web.dev - Interaction to Next Paint (INP) https://web.dev/articles/inp

  2. web.dev - Optimize INP https://web.dev/articles/optimize-inp

  3. Chrome Developers - INP announcement https://developer.chrome.com/blog/inp-cwv

  4. web.dev - Long tasks https://web.dev/articles/optimize-long-tasks

  5. MDN - Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler

  6. web.dev - First Input Delay (FID) - archived https://web.dev/articles/fid