Performance rescue · 2025
Orchard Storefront
- Performance
- Frontend
- Rescue
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.
- Namespace icon import → per-icon imports. 210KB gone in one commit.
- Moment + locales → native `Intl.DateTimeFormat`. 160KB gone, and the formatting got more correct.
- Physics carousel → CSS scroll-snap. 120KB gone and it felt better on touch.
- 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.
// 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
CSS scroll-snap over a carousel library
Kept the tag manager
07 — OutcomesWhere it landed
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.