Skip to main content

Core Web Vitals INP: Optimizing Interactivity 2026

Created: February 23, 2026 Larry Qu 4 min read

Introduction

INP (Interaction to Next Paint) replaced FID (First Input Delay) as a Core Web Vital in 2024. It measures overall page interactivity and is critical for user experience. This guide shows how to optimize for INP.


What Is INP?

The Metric

INP measures the time from a user interaction (click, tap, keyboard) to when the browser paints the next frame showing the result of that interaction.

Why INP Matters

Metric Measures Replacement
LCP Load performance -
FID Initial delay Replaced by INP
CLS Visual stability -
INP Overall interactivity New in 2024

A good INP score is 200ms or less.


How INP Works

Interaction Types

  • Click/Tap: Button clicks, menu opens
  • Keyboard: Typing in inputs, shortcuts
  • Scroll: Smooth scrolling (with delay)
  • Drag: Drag and drop operations

Measuring INP

// Using web-vitals library
import { onINP } from 'web-vitals';

onINP(({ value, entries, name }) => {
  console.log(`INP: ${value}ms`);
  
  // Get the interaction that caused INP
  if (entries.length > 0) {
    const entry = entries[entries.length - 1];
    console.log('Interaction type:', entry.interactionType);
    console.log('Processing time:', entry.processingEnd - entry.processingStart);
  }
});

Common INP Issues

1. Long Event Handlers

// ❌ Bad - Heavy computation on main thread
button.addEventListener('click', () => {
  // This blocks the main thread!
  const data = heavyComputation();
  render(data);
});

// ✅ Good - Break up work
button.addEventListener('click', () => {
  // Use requestIdleCallback or setTimeout
  setTimeout(() => {
    const data = heavyComputation();
    render(data);
  }, 0);
});

// ✅ Better - Use Web Worker
button.addEventListener('click', async () => {
  const result = await processInWorker(data);
  render(result);
});

2. Hydration Delays

// ❌ Bad - Large component tree hydration
import { hydrateRoot } from 'react-dom/client';
import App from './App';

hydrateRoot(document, <App />);

// ✅ Good - Lazy hydration
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

3. Large Bundle Blocking

// ❌ Bad - Import everything at once
import * as utils from './utils';
import * as helpers from './helpers';

// ✅ Good - Dynamic imports
async function handleClick() {
  const { formatData } = await import('./utils/format.js');
  render(formatData(data));
}

Optimization Techniques

1. Use requestIdleCallback

function processQueue(items) {
  if (!items.length) return;
  
  function processBatch(deadline) {
    while (items.length && deadline.timeRemaining() > 1) {
      processItem(items.shift());
    }
    
    if (items.length) {
      requestIdleCallback(processBatch);
    }
  }
  
  requestIdleCallback(processBatch);
}

2. Break Up Long Tasks

// ❌ Bad - All in one task
function handleSubmit(data) {
  validate(data);
  transform(data);
  save(data);
  notify(data);
  redirect();
}

// ✅ Good - Yield to main thread
async function handleSubmit(data) {
  await yieldToMain();
  
  const valid = validate(data);
  if (!valid) return;
  
  await yieldToMain();
  
  const transformed = transform(data);
  
  await yieldToMain();
  
  await save(transformed);
  
  await yieldToMain();
  
  notify(transformed);
  redirect();
}

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

3. Optimize React INP

// ❌ Bad - Large component with many handlers
function Table({ data }) {
  return (
    <table>
      {data.map(item => (
        <tr key={item.id} onClick={() => handleClick(item)}>
          {item.cells.map(cell => (
            <td onClick={() => handleCell(cell)}>{cell.value}</td>
          ))}
        </tr>
      ))}
    </table>
  );
}

// ✅ Good - Event delegation
function Table({ data }) {
  const handleClick = useCallback((e) => {
    const target = e.target;
    if (target.tagName === 'TD') {
      handleCell(target.dataset.id);
    } else if (target.tagName === 'TR') {
      handleClick(target.dataset.id);
    }
  }, []);
  
  return (
    <table onClick={handleClick}>
      {data.map(item => (
        <tr key={item.id} data-id={item.id}>
          {item.cells.map(cell => (
            <td data-id={cell.id}>{cell.value}</td>
          ))}
        </tr>
      ))}
    </table>
  );
}

4. Use CSS for Animations

/* ✅ Good - GPU-accelerated animations */
.smooth {
  transform: translateX(0);
  will-change: transform;
}

.smooth:hover {
  transform: translateX(10px);
}

/* ❌ Bad - Triggers layout */
.bad {
  left: 0;
}

Measuring INP

Field Tools

// Chrome User Experience Report
// Available in CrUX API, PageSpeed Insights, Search Console

Lab Tools

# Lighthouse
lighthouse https://example.com --preset=desktop

# Web Vitals extension
# Chrome DevTools Performance panel

Real User Monitoring

// Custom INP tracking
let maxINP = 0;

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.interactionType) {
      const inp = entry.duration;
      if (inp > maxINP) {
        maxINP = inp;
        console.log('New INP:', inp);
        
        // Send to analytics
        sendToAnalytics('INP', inp);
      }
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });

Best Practices Summary

Issue Solution
Long JS tasks Break up with requestIdleCallback
Large bundles Code splitting, lazy loading
Hydration Selective hydration, islands
Event handlers Event delegation, batching
Heavy computations Web Workers
Layout thrashing Use transform/opacity for animations
Main thread blocking Use CSS animations, virtualize lists

External Resources

Documentation

Tools


Key Takeaways

  • INP measures overall page interactivity
  • Target: Under 200ms
  • Break up long JavaScript tasks
  • Use CSS for animations
  • Event delegation reduces listeners
  • Web Workers offload heavy computation
  • Measure in production with RUM

Resources

Comments

Share this article

Scan to read on mobile