Cover Image

Chrome's own CrUX release notes say it plainly: the continued regression of INP is a cause for concern, and the team has no definitive reason why. In the August 2026 data, 85.3% of origins score good on INP, down 0.5 points, while combined Core Web Vitals pass sits at 55.6%. Mobile passes all three at 48% against 56% on desktop (Web Almanac 2025).
Think of INP like restaurant service time: the clock runs from order to food on the table, and three stations can stall it. This tutorial walks the same path Chrome documents: read the field number, attribute the slow interaction to its phase, fix that phase, then verify. You will learn what INP actually measures, why lab scores mislead, and which fix to try first.
💡 Tip: This post is the hands-on fix guide. If you want the audit checklist that surfaces INP alongside accessibility and semantics, read how to audit AI-generated frontend code first.
Key Takeaways
INP thresholds have not changed since March 2024: good at or under 200 ms, needs improvement up to 500 ms, poor above, evaluated at the 75th percentile of page views, using each view’s INP value.
85.3% of origins pass INP in August 2026 CrUX data, down 0.5 points, and Chrome says the regression has no definitive explanation.
CrUX reports field data over a rolling 28-day window, including the 75th percentile; this is not an arithmetic average or a standalone ranking score. Lab scores guide, never substitute.
Attribute before touching code: input delay, processing duration, and presentation delay each have different fixes, and the web-vitals attribution build exposes all three.
If input delay dominates your trace, consider breaking up long tasks with
scheduler.yield()and a fallback.New observations replace older data across the 28-day window; the timing and size of a visible change depend on traffic and rollout.
Start with the definition, because two popular numbers about it are wrong.
What INP optimization actually measures
INP stands for Interaction to Next Paint. It measures the latency of all clicks, taps, and key presses across a page's lifespan, not just the first one. Scrolling, hover, and zooming are excluded. The thresholds are good at or under 200 ms, needs improvement up to 500 ms, poor above that, evaluated at the 75th percentile of page views. Chrome drops the single highest interaction for every 50 interactions to blunt outliers. INP replaced First Input Delay on 12 March 2024.
The February 2024 CrUX release notes reported 48.8% of origins passing all three metrics when using FID and 45.6% when using INP, a difference of 3.2 percentage points in that dataset.
Three more facts keep you out of trouble. Thresholds have not changed since March 2024. LCP is still 2.5 seconds, and the cited documentation retains that threshold. The cited documentation continues to define INP as a Core Web Vital; it does not establish the absence of all future-metric discussions. And Soft Navigations, now two performance APIs enabled by default from Chrome 151, is an extension of measurement to single-page-app navigations, not a new vital.
Definitions settled, the next mistake is where people read the number.
Start from field data, not the lab
CrUX reports the 75th percentile over a rolling 28-day field-data window, with a processing delay. Google uses Core Web Vitals in ranking systems alongside other signals; there is no single INP number that determines your rank. A lab run is a single synthetic load on one machine. It can guide you, but it is not a substitute. Field INP reflects real devices, real networks, and real interaction patterns your laptop never reproduces.
That gap bites in a specific way. A standard Lighthouse navigation run does not measure INP; an interactive DevTools session or suitable user-flow measurement can collect local interaction data, but a good lab number does not guarantee a good field number. If nobody interacts during the test, there is no INP to report at all. So the workflow is fixed: detect the problem in field data, reproduce locally, fix, then wait for the field to confirm.
Two context numbers worth keeping. The Web Almanac 2025, still the latest edition with July 2025 data, has mobile INP good at 77% against 97% on desktop, a gap narrowed from 23 to 20 points year over year. And on ranking, Google's current framing is deliberately un-hyped: Core Web Vitals are used by ranking systems, but there is no single signal, good scores don't guarantee top placement, and chasing a perfect score for SEO alone may not be the best use of your time. Relevance still wins over polish.
Field data tells you that a page is slow. Attribution tells you where.
Attribute the slow interaction before touching code
Every slow interaction splits into three phases with official names: input delay (time before any callback runs), processing duration (time callbacks execute), and presentation delay (time until the frame paints). Each phase has different fixes, so attributing correctly is most of the work.
The web-vitals library, through its documented attribution build, exposes exactly those three timings through its attribution build:
import { onINP } from 'web-vitals/attribution';
onINP(({ value, attribution }) => {
console.log(
value,
attribution.inputDelay,
attribution.processingDuration,
attribution.presentationDelay
);
}, { reportAllChanges: true });
For deeper cases, the Long Animation Frames API is Chrome's recommended attribution path. It measures frames rather than individual tasks at the same 50 ms threshold as the Long Tasks API, with per-script attribution down to source URL and function name for scripts over 5 ms. It shipped in Chrome 123 and Edge 123; Firefox and Safari do not support it. The older Long Tasks API is not deprecated, but LoAF is the one Chrome points INP work toward.
In DevTools, the documented workflow is: watch the live metrics view's interactions log for a bad interaction, and if it reproduces, record a trace. Expanding an entry on the Interactions track shows the time split across the three phases. Use the interaction and main-thread tracks available in your installed DevTools version. Long Animation Frame entries can also be collected through PerformanceObserver; do not assume every tooling version provides the same visualization.
🚀 Pro tip: If the attribution says input delay dominates, skip every CSS audit and go straight to the next section. Phase-first debugging beats checklist debugging.
With the phase known, fixes come in priority order, according to the phase that dominates the observed interaction. The official playbook is web.dev's Optimize INP; the three sections below compress it to what moves field numbers.
Fix input delay: yield the main thread
Input delay means the browser wanted to handle an interaction and the main thread was busy. The canonical cause is a long task, anything over 50 ms, blocking the queue. The fix is yielding: breaking work into chunks and handing the thread back so pending input gets served.
The yield pattern
Chrome’s long-task guidance recommends yielding between work chunks without gating on isInputPending(). Feature-detect scheduler.yield() and provide a fallback; a single expensive work item may need to be split further or moved to a worker.
function yieldToMain() {
if (globalThis.scheduler?.yield) return globalThis.scheduler.yield();
return new Promise(resolve => setTimeout(resolve, 0));
}
async function processItems(items) {
let chunkStart = performance.now();
for (const item of items) {
doWork(item); // Each item must itself be bounded.
if (performance.now() - chunkStart >= 20) {
await yieldToMain();
chunkStart = performance.now();
}
}
}Support is the catch
Browser support is where this gets awkward: caniuse shows Chrome and Edge from version 129, Firefox from 142, Opera from 115, and no Safari support. Ship a setTimeout fallback for browsers without the scheduler API, and re-check support at publish time rather than trusting this paragraph.
Measure whether yielding improves the interactions whose input delay is dominated by long tasks. Long tasks can delay input processing; yielding gives the browser a chance to handle other work. Defer anything that doesn't need to run before the interaction completes: analytics flushes, non-critical renders, prefetch work. Do it after the browser has painted its response.
Input delay handled, the next suspect is the handler itself.
Fix processing duration: shrink the callback
Processing duration is the time your own event callbacks spend executing. The pattern is familiar: a click handler that re-renders a list, recalculates filters, and writes to the DOM before returning. Every millisecond there delays the paint.
Batch, debounce, transition
Three techniques cover most cases. First, group necessary DOM reads before writes rather than interleaving them; requestAnimationFrame alone does not remove layout work. Read-then-write in a loop forces a synchronous layout on every iteration. Second, debounce or throttle handlers that fire rapidly, but verify the trailing call still runs; a debounced search that drops the final keystroke trades correctness for speed. Third, in React, wrap non-urgent updates so they don't block the interaction:
import { startTransition } from 'react';
function onSearch(query) {
setInput(query); // Urgent controlled-input update.
startTransition(() => {
setResultsQuery(query); // State read by a separate results component.
});
}
// The transition callback runs immediately. Split heavy synchronous
// computation into chunks or move it to a worker.
Layout thrashing deserves its own sentence because it hides inside innocent code. A geometry read after a layout-invalidating change can force synchronous layout; in a loop that becomes a significant source of processing time. The DevTools trace shows it as repeated style-and-layout blocks inside one interaction. Once you see the shape, you stop writing the loop.
Handler time under control, what remains is paint.
Fix presentation delay: trim DOM, CSS, and third parties
The tree and the selectors
Presentation delay is everything after your callbacks finish: style recalculation, layout, paint, composite. Inspect DOM size, style recalculation, layout, and paint work in the trace. Bigger DOMs correlate with slower paint in field data, and Lighthouse flags enormous document trees outright, so treat element count as a cost. Selector-matching cost depends on the affected elements and browser optimizations; inspect style recalculation in the trace. Prune dead markup, flatten needlessly nested wrappers, and scope selectors to classes the browser can match cheaply.
Move non-critical CSS off the critical path with async loading, and precompute layout-critical styles rather than calculating them at interaction time. These are medium-effort changes, but on pages where attribution shows presentation delay dominating, measure their effect on the actual bottleneck.
Then third parties
Third-party scripts (analytics, consent managers, chat widgets, embeds) can add main-thread work and delay interactions. Check their contribution in your trace. Audit with the DevTools Coverage tab to find shipped code that never executes, preserve any required consent checks while deferring optional third-party work, and replace heavy embeds with lite or click-to-load versions. Prioritize third-party work when your trace shows it delaying the interaction.
A word on hydration, since single-page apps ask. Hydration can add main-thread work; measure its contribution in your own application. Partial hydration, server components, and island architectures are reasonable directions with honest engineering trade-offs. Present them as architecture choices, not measured INP wins.
All three phases fixed, one job remains: proving it worked.
Verify locally, confirm in the field
Re-run your local reproduction first and expect a visible improvement. If the trace does not improve, investigate further; one local reproduction does not determine every field outcome. Then deploy and wait, because field data is slow by design. CrUX’s 28-day window gradually replaces older observations. A complete post-change window needs 28 days after rollout reaches the measured users, plus processing time; a visible improvement is not guaranteed. Fix on Monday, and patience isn't optional. It's structural.
Keep the web-vitals attribution snippet from earlier running in production so the next regression arrives with its phase attached. When INP slips again, and per Chrome's own notes the metric is drifting site-wide, you will know within one deploy whether the cause is input delay, processing, or paint, instead of re-running the whole investigation.
💡 Tip: Track INP per template, not just per page. A site-wide regression that no single page explains is usually a shared component: a consent banner, a chat widget, a framework upgrade. Template-level attribution finds it in one query instead of fifty.
Conclusion
INP optimization is a four-step loop: read the field number, attribute the slow interaction to its phase, fix that phase, and let the 28-day window confirm it. Thresholds are stable at 200 and 500 ms, lab scores guide but never substitute, and the phase dominating your trace is the first target to investigate.
Start with your worst page in CrUX, run one bad interaction through the attribution snippet, and fix whatever phase dominates. If it says input delay, yield. If it says processing, shrink the callback. If it says paint, trim the tree.
Andy Phan
Let's connect and embark on this exciting tech journey together! 🌐💻#TechEnthusiast #WebOptimization