Interaction to Next Paint (INP) replaced FID as a Core Web Vitals metric in 2024. INP above 200ms at the 75th percentile is a failing score that suppresses Google search ranking (ISO-25010 time-behaviour). The root cause is always the same: JavaScript running synchronously on the main thread — form submit handlers doing JSON parsing, click handlers firing analytics events, data table sorts blocking the UI. Every millisecond the main thread is busy is a millisecond the browser cannot render the response to the user's action, making the app feel broken.
High because INP above 200ms is a failing Core Web Vitals score, signals unresponsive UI to users, and directly hurts search ranking as of 2024.
Keep event handlers lightweight by offloading heavy work off the main thread. Two primary tools:
scheduler.yield() to break up long synchronous loops:async function processLargeDataset(items: Item[]) {
for (let i = 0; i < items.length; i++) {
processItem(items[i])
if (i % 100 === 0) await scheduler.yield() // yield to browser between chunks
}
}
// src/workers/compute.worker.ts
self.addEventListener('message', (e) => {
const result = expensiveOperation(e.data)
self.postMessage(result)
})
// src/components/table.tsx
const worker = new Worker(new URL('../workers/compute.worker.ts', import.meta.url))
worker.postMessage(largeDataset)
worker.onmessage = (e) => setResults(e.data)
Profile INP in Chrome DevTools → Performance Insights panel to identify which interaction and which function is responsible.
ID: performance-core.rendering-paint.inp-fid-target
Severity: high
What to look for: Count all relevant instances and enumerate each. Check for long-running JavaScript that blocks the main thread. Look for event listeners on interactive elements (buttons, links, form inputs) that might trigger expensive operations. Examine bundle size and third-party scripts that consume main thread time. Check for indicators of optimization: Web Workers, code splitting, or main-thread monitoring.
Pass criteria: Event handlers are lightweight and respond within 100ms. Heavy operations are offloaded to Web Workers or executed asynchronously. Framework has efficient event delegation (no excessive listener setup on every render). Long tasks are profiled and identified.
Fail criteria: Click/input handlers perform heavy computations synchronously (data processing, parsing, encryption). Long tasks over 50ms block user interaction. Slow third-party scripts run on main thread.
Skip (N/A) when: The project has no interactive elements or is a static site.
Detail on fail: Identify the blocking operations. Example: "Form submit handler parses 5MB JSON synchronously; measured delay 340ms. Three third-party scripts (analytics, widget, ads) add 200ms main thread blocking" or "Data table sort operation blocks UI for 150ms; no pagination or virtualization".
Remediation: Input delay is the time between user interaction and response. Reduce it by keeping event handlers lightweight:
Offload heavy work to Web Workers:
const worker = new Worker('heavy-compute.worker.js')
worker.postMessage({ data: largeArray })
worker.onmessage = (e) => setResult(e.data)
Break long tasks with scheduler.yield():
async function processLargeDataset() {
for (let i = 0; i < items.length; i++) {
processItem(items[i])
if (i % 100 === 0) await scheduler.yield()
}
}
Defer non-critical work:
button.addEventListener('click', () => {
performCriticalWork() // runs immediately
requestIdleCallback(() => performNonCriticalWork()) // deferred
})