How to Improve Interaction to Next Paint (INP) Score

INP (Interaction to Next Paint) replaced FID as a Core Web Vital in March 2024, joining LCP and CLS as the three metrics Google uses to measure user experience. It's a harder metric to pass, and a lot of sites that were fine with FID are now failing INP.

A good INP is under 200ms. Anything over 500ms is poor. Unlike FID which only measured the first interaction, INP measures all interactions throughout the page's lifetime and reports the worst one (roughly).

What INP actually measures

When a user clicks a button, taps a link, or presses a key, three things happen:

  1. Input delay - Time from interaction to when event handlers start running
  2. Processing time - Time spent running your JavaScript handlers
  3. Presentation delay - Time from handlers finishing to the next frame being painted

INP is the sum of all three. If any part is slow, your INP suffers.

The key insight: INP penalizes any slow interaction, not just the first one. A page that loads fast but has a sluggish dropdown menu will fail INP.

Find your slow interactions

Option 1: DevTools

  1. Open Chrome DevTools
  2. Go to Performance tab
  3. Record while interacting with the page
  4. Look for long tasks (red corners) during interactions

Option 2: Web Vitals Extension

Install the Web Vitals Chrome extension. It shows INP in real-time as you interact with the page.

Option 3: PageSpeedFix

Run your URL through PageSpeedFix - we identify interaction issues and prioritize them by impact.

The 4 main causes of poor INP

1. Long JavaScript tasks

The browser can't respond to interactions while JavaScript is running. If you have a function that takes 300ms, any click during that time will feel sluggish.

What to do:

Break long tasks into smaller chunks using setTimeout or requestIdleCallback:

// Bad - blocks for entire duration
function processItems(items) {
  items.forEach(item => heavyOperation(item));
}

// Better - yields to browser between chunks
async function processItems(items) {
  const chunks = chunkArray(items, 50);
  for (const chunk of chunks) {
    chunk.forEach(item => heavyOperation(item));
    await new Promise(r => setTimeout(r, 0));
  }
}

Or use a web worker for heavy computation:

const worker = new Worker('/heavy-task-worker.js');
worker.postMessage(data);
worker.onmessage = (e) => handleResult(e.data);

2. Too many event handlers

Every interaction triggers a cascade of event handlers. If you have handlers on multiple ancestors (event bubbling), they all run.

What to do:

Use event delegation instead of individual handlers:

// Bad - handler on every button
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick);
});

// Better - single handler on parent
document.querySelector('.btn-container').addEventListener('click', (e) => {
  if (e.target.matches('.btn')) handleClick(e);
});

Also, remove handlers you don't need. React's synthetic events can accumulate - make sure you're cleaning up on unmount.

3. Forced layout recalculations

Reading layout properties (like offsetHeight) after writing them forces the browser to recalculate layout synchronously. This is called "layout thrashing."

What to do:

Batch your reads and writes:

// Bad - forces layout recalc on every iteration
elements.forEach(el => {
  const height = el.offsetHeight; // Read
  el.style.height = height + 10 + 'px'; // Write
});

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

Use requestAnimationFrame for visual updates:

requestAnimationFrame(() => {
  element.style.transform = 'translateX(100px)';
});

4. Third-party scripts

Analytics, chat widgets, and ad scripts often run heavy code on interactions. They hook into click events and can add significant delay.

What to do:

  • Audit your third-party scripts with Request Map
  • Lazy load non-critical scripts
  • Use loading="lazy" for embedded content
  • Consider self-hosting critical third-party code
<!-- Bad - loads immediately -->
<script src="https://chat-widget.com/widget.js"></script>

<!-- Better - loads on interaction -->
<button onclick="loadChatWidget()">Chat with us</button>
<script>
function loadChatWidget() {
  const script = document.createElement('script');
  script.src = 'https://chat-widget.com/widget.js';
  document.body.appendChild(script);
}
</script>

Framework-specific tips

React:

  • Use useMemo and useCallback to prevent unnecessary re-renders
  • Use React.lazy() for code splitting
  • Avoid inline function definitions in JSX

Next.js:

  • Use dynamic imports: dynamic(() => import('./HeavyComponent'))
  • Leverage server components (RSC) to reduce client JS

Nuxt:

  • Use <ClientOnly> for client-heavy components
  • Lazy load with defineAsyncComponent

Quick checklist

  • Are there any JavaScript tasks over 50ms during interactions?
  • Are you using event delegation where possible?
  • Are layout reads and writes batched?
  • Are third-party scripts lazy loaded?
  • Are heavy computations moved to web workers?

Frequently Asked Questions

What is a good INP score?

A good INP is under 200 milliseconds. Scores between 200-500ms need improvement, and anything over 500ms is considered poor. INP measures responsiveness across all user interactions, not just the first one.

What's the difference between INP and FID?

FID (First Input Delay) only measured the delay before the first interaction. INP measures all interactions throughout the page lifecycle and reports the worst one. This makes INP harder to pass but more representative of real user experience.

Why is my INP failing when my site feels fast?

INP catches slow interactions that happen after initial load - like sluggish dropdown menus, slow form submissions, or laggy scrolling. Your site might load fast but have interaction issues that only INP reveals.

How do I find slow interactions?

Run your site through PageSpeedFix to identify interaction bottlenecks. You can also use Chrome DevTools Performance panel - record while interacting and look for long tasks (functions taking over 50ms).

Do third-party scripts affect INP?

Yes, significantly. Analytics, chat widgets, and ad scripts often hook into click events and run heavy code. Audit your third-party scripts and lazy load non-critical ones to improve INP.

What's next

Want to identify exactly which interactions are slow on your site? Run your URL through PageSpeedFix - we'll show you the specific issues and give you framework-aware fixes.