A checkout that stopped losing sales to a 6-second load
Profiling found the real bottleneck behind a slow checkout (an N+1 query and an oversized bundle) and tuned both, with before-and-after metrics locked in as a baseline against future regressions.
How the work was scoped
- Industry
- E-commerce & Retail
- Duration
- 5 weeks
- Cooperation model
- Fixed price
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
What went wrong, and when
- 01
Nobody had profiled the actual bottleneck. The team assumed it was image weight, but the real cost was an N+1 query firing per cart item and a JavaScript bundle that had grown unchecked for a year.
There were two unrelated causes of roughly equal size. On the server, the cart page issued one product lookup per line item: invisible on a two-item test cart, ruinous on a real one. On the client, a JavaScript bundle had grown for a year without anyone looking, and it blocked render. Fixing either alone would have moved the number by about a third and looked like a failure.
We profiled Core Web Vitals in the browser and query time on the server, found the N+1 pattern and the render-blocking bundle, fixed both, and recorded the improved metrics as a baseline so future regressions get caught early.
Phase by phase
Phase 1: Measure
Against real traffic
Profiled Core Web Vitals in the browser and query time on the server using production traffic, not a synthetic ideal.
- Vitals baseline
- Query profile
Phase 2: Isolate
One query per cart item
Traced the slow checkout to an N+1 query pattern on the checkout path and an oversized render-blocking bundle.
- Root-cause analysis
- Bundle report
Phase 3: Fix
Query and bundle
Fixed the query pattern and split the bundle so the checkout path stopped loading code it didn't need.
- Query fix
- Split bundle
Phase 4: Lock it in
So it stays fixed
Recorded the improved metrics as a monitored baseline with alerting on regression.
- Metric baseline
- Regression alerts
The checkout route's JavaScript before and after the split, module by module: the packs it never needed moved to their own routes and the render-blocking work deferred.
The numbers, before and after
6.1s → 1.8s
Time to interactive
−82%
Checkout query time
−19%
Cart abandonment
Time to interactive is field data from real sessions at comparable traffic, before and after; it isn't a Lighthouse score. Query time is the checkout path's server timing at the ninetieth percentile. Cart abandonment compares the six weeks after with the six before, short enough that seasonality is a caveat, not a correction.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
The engagement
The checkout page was taking upward of six seconds to become interactive during peak traffic, and the team had already guessed at three different fixes that didn't move the number.
An online retailer whose checkout took upward of six seconds to become interactive at peak, and whose team had already shipped three fixes aimed at image weight without moving the number. The engagement ran five weeks and began with one condition: nothing would change until both ends had been profiled, because three unmeasured attempts had already been spent.
Performance Engineering
How it was handled
Profiled frontend Core Web Vitals and backend query time against real traffic
Profiling was made a precondition, because three unmeasured fixes had already shipped against the wrong cause.
Isolated an N+1 query firing once per cart item on the checkout path
The N+1 was found in the query profiler, then collapsed into one batched query with the relations loaded eagerly.
Fixed the query pattern and split the oversized JavaScript bundle
The bundle was split and the render-blocking work deferred: the half of the six seconds the server fix could never have reached.
Recorded before-and-after metrics as a regression baseline
Before-and-after metrics were committed as a baseline with synthetic monitoring running the real checkout journey against it on a schedule.
Profiled, not guessed
Frontend vitals and backend query time measured against real traffic before anything was changed.
Nothing was changed until both ends had been measured under real traffic. Web Vitals came from actual sessions instead of a lab run on a fast laptop, and server timing was profiled per request, so a slow page could be pinned on render, network or the database instead of argued about. Both profiles contradicted the team's expectations, which is exactly why the step exists.
- Field Web Vitals from real sessions, never a lab run
- Per-request server timing to separate render from query cost
- Both profiles contradicted the assumed cause
The profiling session before any change: field Web Vitals from real sessions beside per-request server timing, one checkout load attributed to database, network and render, and the two causes found in place of the assumed one.
One checkout request opened up: 138 queries, the N+1 firing once per cart item, the batched query with eager relations that replaced it, and the test that pins the query count for a ten-item cart.
The actual bottleneck
An N+1 query firing once per cart item, found in the profiler instead of assumed.
The query profiler showed the cart page issuing one product lookup per line item: invisible on a two-item test cart, ruinous on a real one. It was collapsed into a single batched query with the relations loaded eagerly, and a test now asserts the query count for a ten-item cart, so the pattern can't creep back the next time someone adds a field to the loop.
- N+1 caught in the profiler, not deduced from the code
- Collapsed into one batched query with eager relations
- A test asserts query count for a ten-item cart
A regression baseline
The before-and-after figures committed as a baseline, the real checkout journey run on a schedule against it, and what alerts on a percentage regression.
Before-and-after metrics recorded, so a future regression gets caught by the pipeline before a customer finds it.
The before-and-after numbers were committed as a baseline, and synthetic monitoring now runs the same checkout journey on a schedule and alerts on a percentage regression against it. Performance stops being something someone notices during a bad week and becomes something the pipeline reports. That's the only version of this work that survives six months of feature delivery.
- Before and after committed as a baseline, not a slide
- Synthetic runs of the real checkout journey on a schedule
- Alerts on percentage regression against the recorded baseline
Working inside their operation
- 01
A cross-functional team of 4 worked on a fixed price basis over 5 weeks, covering Performance profiling, Query & bundle optimization, Baseline monitoring. We shipped in two-week increments, each one releasable and reviewed with the client before it merged. Decisions were recorded as they were made, so the reasoning survived the people who made it.
Five weeks, fixed price, with measurement as a precondition: nothing could change until both ends had been profiled, because three unmeasured fixes had already been spent. The team sat in on the profiling session, and that's what made the result persuasive. They watched their own assumption fail with their own eyes.
What changed in the runbook
The six seconds were two unrelated causes; fixing either alone would have looked like failure.
Two causes of similar size is why the previous attempts failed: each candidate fix was tested alone, moved the number by a third, and was judged not to be the problem.
Profiling against real traffic found what a synthetic test had been missing for months.
A synthetic test on a fast connection with a two-item cart never reproduced either fault. The conditions that produced them are the ones real traffic supplies.
Without the baseline the fix would have quietly eroded. The monitoring is the durable part.
Performance work erodes silently, so the alert against a committed baseline is what survives six months of feature delivery. The fixes alone wouldn't have.
One loop, three carts
A two-item test cart hid what a real cart paid for
The same checkout request at three cart sizes, with the per-item lookup it used to make and the batched query it makes now. Below it, why neither fix on its own looked like the answer. Switch tabs, or use the arrow keys once one is focused.
Queries per checkout request, by cart size
The request opened up in the profiler: 138 queries, 120 of them the same five lookups repeated once per cart item. Collapsed into one batched query with the relations loaded eagerly, it's nineteen however full the cart is.
138queries fired
18 fixed, then a lookup and four lazy relations for each of 24 items: 120 from the loop.
19queries fired
18 fixed, then one query for all 24 lines with the relations loaded eagerly.
Time to interactive, one fix or both
Two causes of similar size, fixed together: time to interactive from 6.1s to 1.8s, inside the 2.0s budget. Tested one at a time, each fix had looked like it wasn't the problem.
2.0s budget · scale 0–7s · one-fix bars drawn at “about a third”, not measured
From a real shopper's session to the alert that keeps checkout fast
Nothing was changed until both ends had been measured, and nothing measured was left as a one-off. The same path that found the two causes now watches for either one coming back.
- 01 · MeasureField vitals + server timingWeb Vitals from actual sessions, never a lab run on a fast laptop; server timing per request, so time is attributed to render, network or database.
- 02 · IsolateQuery profiler + bundle analyzerThe N+1 was caught in the profiler, firing once per cart item. Nobody had to deduce it from the code.
- 03 · FixBatch, split, deferOne batched query with relations loaded eagerly; the bundle split and render-blocking work deferred. Both, because either alone moved about a third.
- 04 · RecordCommitted baselineBefore and after committed to the repository as a baseline, with a test asserting the query count for a ten-item cart.
- 05 · GuardScheduled synthetic checkoutThe real checkout journey runs on a schedule and alerts on a percentage regression against the baseline.
So checkout stays fast
Measured changes & regression guards
Profiled before anything changed
Nothing was changed until both ends had been measured under real traffic: Web Vitals from actual sessions and server timing per request. Three unmeasured fixes had already been shipped against the wrong cause.
The N+1 can't creep back
A test asserts the query count for a ten-item cart, so the next time someone adds a field to the cart-line loop, the per-item pattern fails the test instead of reaching shoppers.
Regression caught, not discovered
The before-and-after figures are a committed baseline. Synthetic monitoring runs the real checkout journey on a schedule and alerts on a percentage regression against it.
Is your checkout slow for a reason nobody has measured yet? Scope your build in three minutes.
Scope your buildNearby engagements
Web PlatformsA reader people finish, and a library that remembers where they stopped
A reading platform for a comics catalog: a browse surface people can actually navigate, a reader that gets out of the way, and a history that puts everyone back on the page they left.
E-commerce & Retail · 18 weeks
Web PlatformsOne number, a whole handset, and a database that keeps up with 125 brands
A metered IMEI lookup service that turns fifteen digits into a device, its specifications and its status. It's sold three ways to three audiences and backed by a device database that maintains itself.
Consumer Electronics · 22 weeks
Web PlatformsOne platform, two trades, and three quarters of the domain never changed
A multi-tenant Laravel platform sold into two trades. Purchasing, sales, returns, expenses, tenancy and roles are one product; only the nouns on top of them change.
E-commerce & Retail · 30 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














