Performance rescue · 2025

Orchard Storefront

Live
  • Performance
  • Frontend
  • Rescue
RoleContract engineer
Duration6 weeks
TeamMe plus the client's two in-house devs
StackNext.js, React, Shopify Storefront API, Cloudflare, Playwright

01 — The problemA storefront that took nine seconds to become interactive on the phones its customers actually owned. No rewrite — the existing app, made fast.

Mobile conversion was roughly a third of desktop. Everyone assumed a design problem. The analytics said otherwise: bounce rate spiked at the exact moment the page was visible but not yet interactive, and the median customer device was several generations behind the team's test phones.

02 — ConstraintsWhat I had to work inside

  • No rewrite. The team had to own the result after I left.
  • Marketing would not give up the third-party tag manager.
  • Every change had to be provable on a mid-range Android over throttled 4G, not on a MacBook.

03 — ApproachMeasure on the device that actually loses money

The first commit was a CI job that ran Lighthouse against a mid-tier device profile with 4x CPU throttling on every pull request, and failed the build if the interaction budget regressed. Before that, "is this faster?" was a matter of opinion. After it, it was a number in the PR.

04 — ApproachDelete JavaScript, in order of cost

The bundle was 890KB gzipped. A dependency treemap found three things doing most of the damage: a full icon package imported as a namespace, a date library with every locale bundled, and a carousel that shipped a physics engine to move some pictures sideways.

  1. Namespace icon import → per-icon imports. 210KB gone in one commit.
  2. Moment + locales → native `Intl.DateTimeFormat`. 160KB gone, and the formatting got more correct.
  3. Physics carousel → CSS scroll-snap. 120KB gone and it felt better on touch.
  4. Tag manager moved behind an idle callback, so it stops competing with the main thread during hydration.

05 — ApproachStop shipping desktop images to phones

Product images were served at 2000px wide regardless of viewport. Correct `sizes` attributes plus AVIF with a WebP fallback cut the median image payload by about four fifths. The hero got `fetchpriority="high"` and an explicit preload; everything below the fold got `loading="lazy"` and hard-coded dimensions to stop the layout shifting under the user's thumb.

tsx
// sizes tells the browser how wide this will *render*, so it can
// pick the right source before layout. getting it wrong silently
// downloads the largest candidate.
<Image
  src={product.image}
  alt={product.title}
  width={800}
  height={800}
  sizes="(max-width: 640px) 92vw, (max-width: 1024px) 45vw, 380px"
  priority={isAboveFold}
/>

06 — DecisionsWhat I chose, and what it cost

Fix in place

ChoseIncremental repair of the existing app
OverGreenfield rebuild
BecauseA rewrite would have taken two quarters and reintroduced the same mistakes with newer dependencies. The problems were identified and bounded.
What it costSome architectural ugliness survived. The cart context still re-renders more than it should and I documented it rather than fixing it.

CSS scroll-snap over a carousel library

ChoseNative scroll-snap
OverKeeping the existing carousel
Because120KB and a main-thread animation loop replaced by about forty lines of CSS the browser runs on the compositor.
What it costLost programmatic easing between slides. Nobody noticed, but it was a real feature removal.

Kept the tag manager

ChoseDefer it behind requestIdleCallback
OverRemoving third-party scripts entirely
BecauseMarketing owns that tool and would have reinstalled it the week after I left. Deferring it was the change that survived.
What it costStill roughly 90KB of third-party code and some attribution events now fire slightly late.

07 — OutcomesWhere it landed

8.9s → 2.4sTime to interactiveMid-tier Android, 4x throttle.
890KB → 240KBJS shippedGzipped, initial route.
0.31 → 0.02Cumulative layout shiftField data, p75.
+34%Mobile conversionSix weeks post-launch vs. prior period.

08 — RetrospectiveWhat I would tell myself at the start

  • Nobody had ever loaded the site on a slow phone. Thirty minutes with a throttled device beat a month of speculation.
  • The regression gate mattered more than any single optimisation, because it is the part that survives handover.
  • A dependency treemap is the highest-leverage twenty minutes in frontend performance work.
  • You will not win a political fight against a marketing tool. Make it cheap instead of making it gone.