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
Comments