Optimizing INP Interaction Latency
A "Add to cart" button took 680ms to respond. Users clicked it twice, got duplicate items, and support tickets spiked. DevTools showed the click handler synchronously recalculated cart totals across 400 DOM nodes, reflowed the sidebar, and fired three analytics events — all on the main thread before the button's loading state rendered. Yielding the heavy work and deferring analytics dropped INP to 95ms.
How INP is measured
INP captures the latency of all interactions (click, tap, key press) and reports the worst one:
User click → Event dispatch → Handler execution → Style/layout → Paint → INP
Every phase adds to the total. A 10ms handler with 300ms of layout and paint still produces poor INP.
Identifying long tasks
Open DevTools → Performance → record → interact → stop. Look for red triangles marking tasks over 50ms.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn(`Long task: ${entry.duration.toFixed(0)}ms`);
}
}
});
observer.observe({ type: 'longtask', buffered: true });
Optimizing event handlers
Show visual feedback first, then do heavy work:
button.addEventListener('click', async () => {
button.classList.add('loading');
await new Promise(r => requestAnimationFrame(r));
const result = await processInChunks(data);
updateUI(result);
button.classList.remove('loading');
});
Breaking up long tasks
async function processInChunks(items, chunkSize = 50) {
const results = [];
for (let i = 0; i < items.length; i += chunkSize) {
results.push(...items.slice(i, i + chunkSize).map(processItem));
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
}
return results;
}
Deferring non-critical work
button.addEventListener('click', () => {
showConfirmation();
requestIdleCallback(() => {
analytics.track('button_click');
});
});
Analytics, logging, and non-visible state updates should never block interaction feedback.
Offloading to web workers
Move computation off the main thread for data processing, filtering, and sorting. Workers can't touch the DOM but handle the heavy lifting before the main thread applies results.
Third-party script impact
Chat widgets, A/B testing frameworks, and tag managers often register global event listeners that add latency to every interaction. Defer third-party loading and audit with DevTools Event Listener breakpoints.
INP optimization checklist
- Measure field INP with web-vitals or CrUX
- Profile interactions in DevTools Performance panel
- Show visual feedback immediately
- Break up long tasks — chunk processing, scheduler.yield()
- Defer non-critical work — analytics, logging
- Offload computation to web workers
- Audit third-party scripts
Input delay vs. processing vs. presentation
Chrome DevTools breaks INP into subparts: input delay (main thread busy), processing time (your handler), and presentation delay (style/layout/paint). Fix the largest subpart first — they're independent optimizations.
React 19 transitions
Wrap non-urgent state updates in startTransition to keep inputs responsive:
import { startTransition } from 'react';
onChange={(e) => {
setInputValue(e.target.value); // urgent
startTransition(() => filterResults(e.target.value)); // deferred
}}
Debugging checklist
When something doesn't work as documented, verify browser support with Can I use before assuming a polyfill bug. Check the Network tab for failed resource loads, incorrect MIME types, and missing CORS headers. Use the Console for CSP violations and Trusted Types errors that silently block operations.
Compare behavior in incognito mode to rule out extension interference. Test with cache disabled during development but validate with realistic caching in staging. Read the specification for edge cases the tutorial skipped — MDN examples cover happy paths, not every boundary condition.
If performance regresses after deployment, roll back first and investigate second. Keep a changelog of performance-related changes linked to metric dashboards. Future you will need to know why that preload tag exists before removing it during a refactor.
Interaction targets worth profiling first
From CrUX and RUM, prioritize:
- Primary CTA buttons (add to cart, submit, save)
- Menu open/close (hamburger, dropdown)
- Autocomplete and combobox keyboard navigation
- Drag-and-drop alternatives lacking keyboard path (fix a11y + INP together)
scheduler.postTask prioritization
Where supported, user-visible updates get user-blocking priority; background prefetch background:
scheduler.postTask(() => updateUI(), { priority: 'user-blocking' });
Falls back to setTimeout in unsupported browsers—feature detect.
Long task attribution
PerformanceObserver longtask entries plus attribution script URL identify third-party INP culprits — defer or facade load. INP over 200ms p75 on mobile triggers Search Console warning — fix top three interactions first (submit, menu, autocomplete).
scheduler.yield in handlers
Split 150ms+ click handlers with await scheduler.yield() where supported — yields to input. Fallback setTimeout(0) chunking for Safari gaps.
Practical follow-through (1)
Ship the smallest vertical slice first — one route, one widget, one index configuration — with rollback documented before expanding scope. Baseline the user-visible metric this work protects (latency, recall, conversion, task success rate) for seven days before change and seven days after in your largest market.
Compare canary p75 to control before full rollout. Exercise edge paths manually: refresh, back navigation, double-submit, offline mode, and keyboard-only flows. When assumptions change — traffic doubles, vendor upgrades, org restructure — revisit whether the original design still fits; quiet periods hide drift until the next incident.
Practical follow-through (2)
Ship the smallest vertical slice first — one route, one widget, one index configuration — with rollback documented before expanding scope. Baseline the user-visible metric this work protects (latency, recall, conversion, task success rate) for seven days before change and seven days after in your largest market.
Compare canary p75 to control before full rollout. Exercise edge paths manually: refresh, back navigation, double-submit, offline mode, and keyboard-only flows. When assumptions change — traffic doubles, vendor upgrades, org restructure — revisit whether the original design still fits; quiet periods hide drift until the next incident.
Practical follow-through (3)
Ship the smallest vertical slice first — one route, one widget, one index configuration — with rollback documented before expanding scope. Baseline the user-visible metric this work protects (latency, recall, conversion, task success rate) for seven days before change and seven days after in your largest market.
Compare canary p75 to control before full rollout. Exercise edge paths manually: refresh, back navigation, double-submit, offline mode, and keyboard-only flows. When assumptions change — traffic doubles, vendor upgrades, org restructure — revisit whether the original design still fits; quiet periods hide drift until the next incident.
Practical follow-through (4)
Ship the smallest vertical slice first — one route, one widget, one index configuration — with rollback documented before expanding scope. Baseline the user-visible metric this work protects (latency, recall, conversion, task success rate) for seven days before change and seven days after in your largest market.
Compare canary p75 to control before full rollout. Exercise edge paths manually: refresh, back navigation, double-submit, offline mode, and keyboard-only flows. When assumptions change — traffic doubles, vendor upgrades, org restructure — revisit whether the original design still fits; quiet periods hide drift until the next incident.
Practical follow-through (5)
Ship the smallest vertical slice first — one route, one widget, one index configuration — with rollback documented before expanding scope. Baseline the user-visible metric this work protects (latency, recall, conversion, task success rate) for seven days before change and seven days after in your largest market.
Compare canary p75 to control before full rollout. Exercise edge paths manually: refresh, back navigation, double-submit, offline mode, and keyboard-only flows. When assumptions change — traffic doubles, vendor upgrades, org restructure — revisit whether the original design still fits; quiet periods hide drift until the next incident.
Resources
- web.dev: Optimize INP
- web.dev: INP metric definition
- Long Animation Frames API
- Scheduler API (MDN)
- web-vitals attribution
Frequently asked questions
What replaced First Input Delay as a Core Web Vital?
Interaction to Next Paint (INP) replaced FID in March 2024. FID only measured the delay before the first event handler ran. INP measures the full interaction latency — from input to the next painted frame — across all interactions on the page. It captures slow handlers, layout thrashing, and rendering delays that FID missed entirely.
What INP score should I target?
Under 200 milliseconds at the 75th percentile is good. Between 200ms and 500ms needs improvement. Above 500ms is poor. INP reports the worst interaction latency (or 98th percentile in some tools) excluding outliers. Focus on the interaction types with the highest latency — usually clicks on buttons that trigger heavy JavaScript.
How do I find which interactions cause poor INP?
Use Chrome DevTools Performance panel with Web Vitals enabled, or the Long Animation Frames API. Record a session, interact with the page, and look for long tasks over 50ms immediately after input events. The web-vitals library reports INP with attribution data showing which event type and target element caused the slow interaction.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →