Back to Lab
RAXXO Studios 9 min read No time? Make it a 1 min read

Six Months of Shopify Web Vitals Data What Actually Moved INP

Development
9 min read
TLDR
×
  • INP dropped from 380ms to 140ms over six months on raxxo.shop
  • Splitting one heavy event handler cut 110ms by itself
  • Lazy-loading the cart drawer JS moved INP 80ms
  • Deferring third-party analytics did nothing measurable

INP went from 380ms to 140ms on raxxo.shop over six months. Three changes did almost all the work. Two changes I was sure would help did nothing. Here is the real field data, not a synthetic Lighthouse score from my laptop.

Why INP Replaced The Metric I Used To Chase

For two years I treated Largest Contentful Paint as the only number that mattered. I optimized images, preloaded fonts, and trimmed render-blocking CSS until LCP sat under 2 seconds. Then Interaction to Next Paint became a Core Web Vital in March 2024, and my green scores turned yellow overnight.

INP measures how long the page takes to visually respond after a tap or click. Not the first interaction. The worst one, roughly, across the whole visit. A store can paint fast and still feel broken if tapping the cart button freezes the thread for 400ms. That lag is invisible in Lighthouse because Lighthouse does not click things the way a real shopper does.

My starting point in field data (Chrome User Experience Report, real Android and iPhone traffic) was a 75th percentile INP of 380ms. Google's "good" threshold is 200ms. So a quarter of my visitors were getting something slower than 380ms, and the median experience was sluggish on mid-range phones. Desktop was fine. Desktop is always fine. Nobody shops on a desktop with a 2019 Android phone's CPU.

The thing that surprised me: my LCP was 1.9 seconds, technically good, and yet the store felt slow when you actually used it. LCP measures loading. INP measures responsiveness. They are different problems with different fixes, and the fixes for one rarely help the other.

I pulled six months of CrUX data plus my own Real User Monitoring (RUM) script that logs every interaction's duration to a tiny endpoint. Synthetic tools lie about INP because they run on hardware faster than what most people carry. The only honest number comes from the field. If you want the broader performance story, Shopify Theme Performance From 62 to 98 Lighthouse covers the load-time side, but that work barely touched INP. I had to start over with a different mental model: stop counting bytes, start counting milliseconds of blocked main thread.

The main thread is the bottleneck. Every tap waits in line behind JavaScript already running. Fix the line, fix INP.

The Three Changes That Actually Moved The Number

Change one: I split a single heavy event handler. My theme had one click listener attached to the document that ran a long if-else chain on every tap anywhere on the page. It checked for add-to-cart, quick-view, menu toggle, swatch selection, and eight other cases. One tap meant running all that logic.

I broke it into scoped listeners attached to specific elements, and I moved the expensive part (recalculating cart totals and re-rendering the drawer) behind a `requestIdleCallback` so it ran after the paint instead of before it. INP dropped from 380ms to 270ms. That single change was worth 110ms. I almost did not believe the RUM data, so I waited three weeks. The number held.

Change two: I lazy-loaded the cart drawer JavaScript. The drawer code, about 34KB of parsing and execution, ran on every page even if the visitor never opened the cart. On a product page that JS competed with everything else for the main thread. I changed it to load only on first interaction with any cart trigger. The first cart open is now 50ms slower (acceptable, it is a deliberate action), but every other interaction on the page got faster because the thread was not pre-loaded with drawer logic. INP moved from 270ms to 190ms. Worth 80ms.

Change three: I replaced a third-party review widget that injected 90KB of JavaScript and ran layout-thrashing DOM writes on scroll. I swapped it for static rendered HTML pulled at build time, with the interactive "write a review" form lazy-loaded behind a button. This was the biggest single byte reduction but only a medium INP win because the widget's worst offense was scroll jank, which INP partially captures. INP went from 190ms to 140ms. Worth 50ms.

Three changes, 240ms total, and 140ms sits comfortably under the 200ms "good" line with room before the next regression. None of these required rebuilding the theme. They required finding which JavaScript blocked the main thread during interactions and either moving it later or removing it.

If you run Shopify and your theme is a popular paid one, check for a global document click handler first. They are everywhere and they are the cheapest 100ms you will ever recover.

The Two Changes I Was Sure Would Help And Didn't

Change A: I deferred all third-party analytics and tracking pixels. I moved analytics, the pixel, and a heatmap tool to load after the page was interactive, using `requestIdleCallback` and a hard delay. I expected a big INP win because these scripts are notorious. The field data moved by zero milliseconds. Not a regression, not an improvement. Statistical noise.

Why? Because those scripts mostly run once on load and then sit quiet. They hurt LCP and Total Blocking Time at startup, but they were not running during the interactions INP measures. My visitors' slow taps happened 8 seconds into a session, long after analytics finished. I had been blaming the wrong scripts for two years. The deferral helped my load metrics slightly, which is fine, but it taught me a lesson: INP problems live in interaction handlers, not in startup scripts.

Change B: I added `content-visibility: auto` to below-the-fold sections. This is a real CSS feature that skips rendering work for offscreen content. On paper it should reduce main thread work and help everything. In my field data, INP did not budge. It helped initial render slightly (the browser skips layout for hidden sections) but interactions still hit the same heavy JavaScript handlers, and that JavaScript was the bottleneck, not layout.

The pattern across both failures: I was optimizing rendering and loading when my problem was scripting. INP is dominated by long tasks during interaction. CSS tricks and load-time deferral are the wrong tools. They are great tools for the wrong metric.

This is the trap. The advice you read for "speed" is mostly LCP and First Contentful Paint advice. Preload fonts, compress images, defer scripts. All correct, all worth doing, all nearly useless for INP. INP needs you to profile actual interactions in Chrome DevTools, find the long task, and break it up or move it off the critical path.

I covered the upscaling side of asset prep separately, and tools like Magnific handle image quality, but no image change ever moved my INP. Bytes are an LCP story. Milliseconds of blocked thread are the INP story, and you cannot guess which scripts block. You have to measure with RUM on real phones. The two changes that failed both looked smart on paper and produced flat lines in production.

How I Measured This Without Fooling Myself

I trust three data sources and ignore everything else. First, CrUX field data through the Search Console Core Web Vitals report and the CrUX API. This is real Chrome traffic, 28-day rolling window, 75th percentile. It is the number Google actually uses. The downside is the 28-day lag, so a change you ship today does not show clearly for a month.

Second, my own RUM script. About 40 lines of JavaScript using the `PerformanceObserver` API with `type: 'event'` and `durationThreshold: 40`. It logs every interaction over 40ms with the element, the duration, and the device class to a lightweight endpoint. This gives me same-day signal, which CrUX cannot. When I split the event handler, RUM showed the drop within hours. CrUX confirmed it four weeks later.

Third, the Chrome DevTools Performance panel with CPU throttling set to 4x slowdown, recording an actual interaction (open cart, add product, change variant). This is where I find the long task. The flame graph shows exactly which function blocks the thread and for how long. Every one of my three wins started here. I clicked the cart button, saw a 180ms yellow block, expanded it, and found the global handler running its full if-else chain.

The mistake I made for months was trusting the Lighthouse INP estimate in the lab tab. It runs on throttled but still capable hardware and it does not reproduce the real interaction pattern. My lab INP read 120ms while real users sat at 380ms. The lab number made me complacent.

A practical rule: if lab and field disagree by more than 2x, trust the field and go find what the lab is not simulating. For me it was a mid-range Android CPU under thermal load, which no synthetic test reproduces.

I keep a simple spreadsheet. Date, change shipped, RUM INP before, RUM INP after, CrUX confirmation date. Five columns. It stops me from claiming a win I cannot prove. Half of "performance optimization" is discipline about measurement, not cleverness about code. The fancy fix that feels productive often does nothing, and the boring fix you almost skipped recovers 100ms.

Bottom Line

INP is the metric most stores are quietly failing right now. Your LCP can be green while a quarter of your shoppers wait 380ms for the cart to respond on their phones. The fix is not image compression or font preloading. It is finding the JavaScript that blocks the main thread during interactions and either splitting it, deferring it past the paint, or deleting it.

My three wins were a global event handler split (110ms), lazy-loading the cart drawer (80ms), and replacing a heavy review widget (50ms). My two non-wins were deferring analytics and adding `content-visibility`. Both felt productive. Both did nothing for INP, because INP is a scripting problem, not a loading problem.

Measure with field data and a tiny RUM script, not lab scores. If your lab and field numbers disagree by 2x, trust the field.

If you want the full system I use for documenting these experiments and shipping store changes with an AI assistant, the Claude Blueprint walks through how I structure the whole workflow, from profiling to deploy. Start with one DevTools recording of your own cart button. The long task is usually right there.

This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)

Stay in the loop
New tools, drops, and AI experiments. No spam. Unsubscribe anytime.
Back to all articles