← Selected work

Open source · Case study

Shopify Headless Perf Lab

A headless Shopify storefront on Hydrogen, React Router 7 and the Oxygen worker runtime, built so its performance numbers can be regenerated by anyone who clones it.

Both modes are deployed on Cloudflare Workers from the same commit, differing only by one environment variable. Paste either URL into PageSpeed Insights and Google does the measuring — on its hardware, not mine.

The absolute numbers will not match the table further down. That one is loopback with applied throttling; this is a real network from wherever you happen to be. The gap between the two URLs is the same gap, measured by something with no stake in the answer.

Performance results are difficult to revisit after the fact. The slower version of a storefront is usually replaced by the time anyone asks about it, so the numbers survive as a figure with no way to see what was measured, on what hardware, against what network profile, or across how many runs.

I wanted this one to stay checkable — a measurement someone can re-run on their own machine and get the same shape of answer. That needs a slower version that still exists and still runs, which is why the storefront ships in two modes instead of one.

The same storefront ships in two modes, selected at runtime by an environment variable. optimized is how it should go out. baseline re-introduces six regressions, each drawn from a pattern that actually shows up on app-heavy Shopify themes.

All six live in a single table in one file. That is deliberate: it means a reviewer can read one file and know the complete list of differences between the two runs. Scattered through components as if (baseline) branches, the comparison would not be auditable.

LeveroptimizedbaselineWhy it matters
CDN preconnectpresentremovedA TLS round trip before the LCP image can start
Hero imageeager + high prioritylazy + autoLazy-loading the LCP element defeats the preload scanner
sizes hintviewport-awarehardcoded 1600pxMobile downloads a desktop-width image
Layout reservationaspect ratio setabsentThe usual cause of a bad CLS score
Third-party app scriptabsentsynchronous in <head>Reviews / popup / upsell bundles — the biggest real-world cost
Storefront API cacheCacheLong / CacheShortCacheNoneWarm vs cold TTFB

Nothing here is an artificial delay bolted onto the render. The third-party script is served by a real route with real latency and real main-thread cost, so Lighthouse observes it the same way it would observe a reviews widget.

Largest Contentful Paint2,650 ms−47% vs baselinefrom 4,968 ms
Total Blocking Time4 ms−97% vs baselinefrom 143 ms
Lighthouse performance95+19 pointsfrom 76
MetricBaselineOptimizedChange
Largest Contentful Paint4,968 ms2,650 ms−47%
Total Blocking Time143 ms4 ms−97%
Speed Index5,010 ms3,088 ms−38%
Transfer size714 KB473 KB−34%
BaselineOptimizedEach row scaled to its own baseline — lengths compare within a row, not across rows.

Mobile emulation at 4× CPU on Slow 4G, median of three runs per URL, on the homepage route. The throttling is applied, not Lighthouse’s default simulation — simulation replays the trace against a modelled network graph and placed first paint after work that had really run before it, reporting 0 ms of Total Blocking Time in both modes while the trace held a 722 ms task from the third-party script.

One lever does not show up in the numbers.The baseline emits a hero image with no width, height or aspect ratio, and both modes still measure a Cumulative Layout Shift of 0.000 — the mock catalog’s images are small enough, and a preview server on loopback fast enough, that the hero decodes before first paint and nothing is ever laid out twice. The lever stays because the budget still has to hold against a real image origin, but the report says so rather than quietly reporting a win.

$ npm run build
$ npm run perf   # both modes, 3 runs per URL, median
Writes docs/perf/latest.md and latest.json. No Shopify account, no API token, no dev store — the storefront runs against a public mock Storefront API, so it works on a clean machine and in CI with no secrets.

Lighthouse CI runs on every pull request against fixed thresholds, and a regression fails the check. Writing the targets down is the easy half; having them enforced automatically is what makes them hold when a release is due on Friday.

AssertionThresholdLevel
Performance category≥ 0.90error
Accessibility category≥ 0.95error
SEO category≥ 0.95error
Largest Contentful Paint≤ 2,500 mserror
Cumulative Layout Shift≤ 0.10error
Total Blocking Time≤ 300 mserror
Unsized imagesnoneerror

There is an open pull request in the repository that exists only to be rejected. It lazy-loads the hero image and stops reserving its layout box — two mistakes that reach production constantly — and CI blocks it. It is not merged, so the red check stays where anyone can read it.

What this does not cover. The public mock API is read-only, so cart mutations render correctly but no checkout can complete, and Customer Accounts, Markets and Functions are not exercised here. The catalog is small, so nothing in it speaks to behaviour at a hundred thousand SKUs. And lab numbers are a pre-merge gate rather than a substitute for field data — a production storefront should be governed by what real visitors experience.