Bỏ qua để vào nội dung chính
INP Under 200ms: 7 Fixes That Actually Work for Interaction to Next Paint

INP Under 200ms: 7 Fixes That Actually Work for Interaction to Next Paint

Bởi Liam Parker
26 thg 3, 20266 phút đọc

Google's INP metric is now a Core Web Vital and most sites are failing it. Here are 7 targeted fixes—scheduler.yield(), input debouncing, long task breaking, and more—that reliably bring INP under the 200ms good threshold.

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.

Không spam, hủy đăng ký bất kỳ lúc nào.

Bài viết liên quan