Almost every engineering team can quote a Lighthouse score. Far fewer can quote their field INP, and that gap is why checkout pages keep turning up as poor in Search Console a year after a redesign that scored 98 in the lab.
Interaction to Next Paint replaced First Input Delay on 12 March 2024. FID measured one narrow thing: the delay before the first interaction's handler started. A page could pass it and still feel slow for the rest of the visit. INP watches every click, tap and keypress for as long as the page is open, then reports the slowest one, discarding a single outlier for every 50 interactions so one stray frame does not define the score.
The bands are 200ms for good and 500ms for poor, measured at the 75th percentile of real Chrome sessions over a rolling 28 days. Read that p75 number as a statement about your slowest quarter of traffic, not about an average shopper.
Checkout is where the main thread goes to work
Home pages get optimized because everyone looks at them. Checkout decides revenue and is usually the last page anyone profiles. It is also the page with the most happening at once.
- Address autocomplete that fires three requests on the first keystroke and parses the response on the main thread.
- A tag manager loaded at the top of the document, with marketing tags bound to form changes and button clicks.
- Payment SDKs that inject their own iframes, fonts and validation logic after the page has already painted.
- Cart totals recalculated in JavaScript on every field change, inside a DOM with hundreds of line-level nodes.
- A framework hydration pass still finishing when the shopper reaches the promo code field.
Individually, each of those costs tens of milliseconds. Together they queue behind each other on a single thread, and the shopper experiences the sum as a button that does not respond.
Field data first, lab data second
Lighthouse runs once, on a fast machine, with a cold cache and no extensions. It has no idea what a returning shopper's fourth interaction feels like. Chrome's field data does. The number that matters therefore lives in the Core Web Vitals report in Search Console and in the CrUX dataset, and both trail a fix by up to 28 days.
Instrument two things before changing a line of code.
The web-vitals library's attribution build splits each slow interaction into the three phases that make up INP, and names the element that started it.
import { onINP } from 'web-vitals/attribution';
onINP((metric) => {
const { attribution } = metric;
navigator.sendBeacon('/rum', JSON.stringify({
value: metric.value,
target: attribution.interactionTarget,
inputDelay: attribution.inputDelay,
processing: attribution.processingDuration,
presentation: attribution.presentationDelay,
}));
});
Send that to your own endpoint with the page path, device class and connection type attached. Aggregate by template, because a site-wide average hides the one page that fails.
The second instrument is the Event Timing API, which browsers already record. A PerformanceObserver subscribed to event entries gives you the same three phases per interaction, including the ones a sampling vendor drops. For deeper attribution, the Long Animation Frames API names the scripts behind each slow frame and separates style from layout time. It ships in Chrome and Edge 123 and later and is not Baseline, so guard it with PerformanceObserver.supportedEntryTypes.includes('long-animation-frame') and let other browsers skip it.
Then record a session in the Performance panel with the CPU throttled to 4x or 6x, on the real checkout flow with a full cart. If the page feels fine on a laptop at full speed, you have measured the laptop.
The three phases need three different fixes
Every slow interaction breaks into input delay, processing time and presentation delay. Teams lose weeks optimizing the wrong one, usually processing, because that is the phase a flame chart makes obvious.
Input delay: the thread was already busy
This is the gap between the tap and the handler starting. It comes from work already in flight: a hydration pass, a long analytics task, a layout triggered by an animation. Handler tuning does nothing here, because the handler has not run yet. The fix is to stop the page being busy when the shopper arrives. Defer non-critical scripts until after the first interaction, cut the initial JavaScript payload, and keep animation off properties that trigger layout.
Processing time: the handler does too much
Now the click has landed and your code is running. A promo code handler that validates locally, recalculates totals, re-renders a table of line items and updates three analytics tools will pass 200ms on a mid-range Android before it ever reaches the network. Split the work. Paint the state change the shopper expects, yield, then do the accounting. Anything that does not need the main thread, like tax and shipping math over a large cart, belongs in a worker.
Presentation delay: the page cannot paint what it just changed
The handler finished and the browser still has to recalculate style, lay out and paint. A 6,000-node DOM with deeply nested grid containers makes that step expensive, and no amount of JavaScript splitting hides it. Reduce node count, avoid animating width and top, use content-visibility on long lists, and keep the checkout step's markup to what the step actually shows.
INP is not load speed
A page can load in under a second and still feel broken. INP starts where load metrics stop: after the document is interactive and the shopper tries to do something. Stores that obsess over LCP and never look at responsiveness end up with a page that appears instantly and then ignores the first three taps.
The reverse is also true, which is what makes INP worth its own budget. Fixing responsiveness does not require rebuilding the page. It requires deciding what runs on the main thread, and when.
Changes that move the number
Ranked by return on effort, these are the ones worth doing first.
- Hand the frame back before expensive work:
scheduler.yield()lets the pending visual update paint before your next chunk runs. Feature-detect it and fall back to a MessageChannel-based yield where it is missing. - Show the pending state before doing the arithmetic. Disable the button and paint the acknowledgement in the first handler, then await the rest. Shoppers forgive a slow total far more readily than a dead button.
- Keep third parties off the critical path. Chat, heatmaps and testing tools can load on first interaction or after the page settles, and the tag manager does not belong in the head of a checkout document. Every tag needs an owner and a review date.
- Use one delegated listener on the form rather than forty per-field handlers, and mark scroll and touch listeners passive so the browser stops waiting on code that never calls
preventDefault. - Trim the DOM on the step that matters. Virtualise long order summaries, collapse hidden sections, and stop rendering the whole cart inside the payment step.
- Batch reads and writes. Reading
offsetHeightstraight after setting a style forces a synchronous layout, which is exactly the delay INP punishes. Read once, write once, and wrap non-urgent React updates instartTransition.
Things that look like fixes and are not
A faster server moves time to first byte. INP is mostly client CPU, so a hosting upgrade rarely touches it, and a CDN's contribution to interaction latency is close to zero.
More aggressive lazy-loading often makes responsiveness worse. Images that decode during the shopper's first tap compete for the same thread, and a hero image that arrives late is cheaper than a payment form that stutters.
A 98 in Lighthouse is a lab score on a simulated device, not a promise about field data. Treat it as a smoke test. The same applies to a chat widget configured to "load on idle": it still runs on the main thread when it loads, and the only question that matters is whether it loads while the shopper is trying to type.
The extra tax on single-page storefronts
Hydration is the most common source of input delay on modern storefronts, and it is invisible in a Lighthouse run that finishes before the shopper arrives. A client-rendered checkout ships a document, then parses a bundle, then attaches handlers to markup that has been visible for a second. Every tap in that window is queued.
Two changes help most. Render the checkout step on the server so the markup is interactive without waiting for the bundle, and hydrate only the islands that need state: the promo field, the payment switch, the address form. A framework that hydrates the whole page to support one interactive field is charging every shopper for work only some of them use.
The other change is political. If marketing can add a script to checkout without a review, the page regresses within a quarter, and no framework upgrade prevents that.
Testing a fix before it ships
Total Blocking Time is the closest lab proxy for INP, and it is imperfect. It measures main-thread blocking during load, not the interaction a shopper makes forty seconds later. Use it as a regression gate on the bundle, not as proof the fix worked.
For proof, replay the interaction. Record the checkout flow in DevTools, throttle the CPU to 6x, and drive the flow with a script that clicks the promo field, types, applies the code and switches payment method. Watch the three phases in each event entry. A handler that looks fast at 1x will often take 300ms at 6x, and 6x is closer to the phone in the shopper's hand than your laptop is.
Then confirm in the field. Your own RUM shows the change within days. CrUX confirms the direction over 28.
A 30-day sequence that ships
- Week 1, instrument. Ship the attribution build and an Event Timing observer, collect a week of interactions with path, device and connection type, and rank templates by p75. Pick the three worst.
- Week 2, remove. Audit every script on the checkout template. Kill what nobody can justify, defer what survives, and measure the delta before touching application code.
- Week 3, rewrite two handlers. Not twelve. The promo code handler and the payment method switch are usually the worst offenders, and both respond well to paint-first, compute-second.
- Week 4, lock it down. Alert on field p75 above 200ms for the checkout path, add a bundle-size budget to CI, and require an owner and a review date for every new third-party script.
Expect the field number to lag the deploy. CrUX rolls over 28 days, so a fix shipped on the first of the month reads properly by the end of it. Judge the work on your own data within days and use CrUX to confirm the trend, not to grade the release.
Why this keeps coming back
INP regresses quietly because nothing in a normal release process watches it. A tag gets added for a campaign. A component gains a synchronous calculation. A library update hydrates a little later than before. None of that fails a test, and none of it stands out in a code review.
The teams that hold a good score treat responsiveness as a budget with an owner, the way they treat bundle size or query count. That is an organisational change more than a technical one, and it is the only version of this work that lasts longer than the next redesign.
The fix does not require a redesign. It requires knowing which of the three phases is slow on your checkout, fixing that one, and keeping a guard in CI so the next campaign tag does not quietly undo it.