Optimizing INP: How to Fix Interaction to Next Paint in 2026
INP replaced FID as a Core Web Vital. Learn practical techniques to reduce interaction latency and hit the "Good" threshold of under 200ms in real-world apps.
What Is INP and Why It Replaced FID
Interaction to Next Paint (INP) became a Core Web Vital in March 2024, replacing First Input Delay. Unlike FID, which only measured input delay, INP measures the full interaction latency — from user gesture to visual update — across the entire page lifetime.
The thresholds: under 200ms is "Good", 200–500ms needs improvement, and above 500ms is "Poor". Most sites struggle because JavaScript work blocks the main thread between input and paint.
The INP Anatomy
Every interaction has three phases:
- Input delay — time waiting for the main thread to be free
- Processing time — your event handlers running
- Presentation delay — browser rendering the next frame
The most common culprit is long tasks blocking the main thread during input delay.
Break Up Long Tasks with Scheduler API
The modern approach is yielding back to the browser between chunks of work:
// Before: one big synchronous task
function processData(items) {
items.forEach(item => heavyComputation(item));
}
// After: yield between chunks
async function processDataYielding(items) {
for (let i = 0; i < items.length; i++) {
heavyComputation(items[i]);
// Yield every 50 items to let browser handle input
if (i % 50 === 0) {
await scheduler.yield();
}
}
}
The scheduler.yield() API is now available in Chrome and polyfillable with a simple Promise trick for other browsers.
Optimize Event Handlers
Move expensive work out of event handlers using requestAnimationFrame and startTransition:
import { startTransition, useState } from 'react';
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleInput(e) {
// Urgent: update the input immediately
setQuery(e.target.value);
// Non-urgent: defer the expensive search
startTransition(() => {
setResults(performSearch(e.target.value));
});
}
return (
<div>
<input value={query} onInput={handleInput} />
<ResultsList results={results} />
</div>
);
}
Avoid Layout Thrashing
Reading layout properties (like offsetHeight, getBoundingClientRect) after writing DOM styles forces the browser to do synchronous layout. Batch your reads and writes:
// Bad: interleaved read/write causes multiple layouts
elements.forEach(el => {
const height = el.offsetHeight; // READ — forces layout
el.style.height = height * 2 + 'px'; // WRITE
});
// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all READs
heights.forEach((h, i) => {
elements[i].style.height = h * 2 + 'px'; // all WRITEs
});
Use Web Workers for CPU Work
Offload heavy computation entirely off the main thread:
// worker.js
self.onmessage = ({ data }) => {
const result = expensiveTransform(data);
self.postMessage(result);
};
// main thread
const worker = new Worker('./worker.js');
worker.postMessage(bigDataset);
worker.onmessage = ({ data }) => updateUI(data);
Measure with Real User Monitoring
Lab tools like Lighthouse only show synthetic INP. Use the web-vitals library to capture real INP from real users:
import { onINP } from 'web-vitals';
onINP(({ value, rating, entries }) => {
console.log();
// Send to your analytics
sendToAnalytics({ metric: 'INP', value, rating });
});
Key Takeaways
- Use
scheduler.yield()to break up long tasks - Wrap non-urgent state updates in
startTransition - Batch DOM reads before writes to avoid layout thrashing
- Push CPU-heavy work to Web Workers
- Always measure with real user data, not just Lighthouse
INP is the most actionable Core Web Vital because it responds directly to JavaScript optimization. Start with your slowest interactions first.
Admin
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!