Thousands of shops on one storefront, and iPhones that ran out of memory.
- Client
- A hyperlocal marketplace for same-day delivery or in-store pickup from local shops
- Scale
- Thousands of products and shops behind one storefront
- Engagement
- A storefront rebuild on the client's existing back end
The outcome in five lines
| Measure | Before | After | Evidence |
|---|---|---|---|
| Crashes on iPhones with limited memory1 | the app crashed on some iPhones | bundles debugged and cut down to prevent the crashes | Described |
| Thousands of products and shops2 | the storefront struggled to handle them | handled without slowing the pages | Described |
| First load and time to interactive3 | too slow, the client's own brief | pages rendered on the server, with skeletons while data arrives | Described |
| Memory on long product lists4 | leaks when many products were shown | off-screen components kept out of the render path, leaks traced | Described |
| Back end and admin dashboard5 | a Loopback.io API and a React-Admin dashboard | the same two, unchanged, with the new storefront built to them | Described |
All five lines describe a change in how the system works, and none carries a number. Load times, memory use and crash rates were not measured before and after, so no figure is claimed. Each line links to its source.
The problem, in the client's terms
The client runs a marketplace that connects shoppers with local stores for same-day delivery or in-store pickup. Its back end, built on Loopback.io, and its admin dashboard, built on React-Admin, were in place. The storefront was the problem. It had to handle thousands of products and shops, load quickly, and stay up on phones with little memory. Some phones, especially iPhones, did not have the memory, and the app crashed.
For a shopper, that looks like a list of nearby shops that goes blank or reloads halfway down, or a first screen that takes long enough to give up on. Same-day delivery is bought on impulse, so a shopper who gives up usually orders somewhere else.
For the client, every new shop made it worse. Each shop adds products, each product adds to what the phone has to hold, and the phones with the least memory fail first.
- ConstraintThe back end and the admin dashboard stay as they are. Only the storefront changes.
- ConstraintPages must be readable by search engines, not only by browsers that run JavaScript.
- ConstraintOne cart must hold items from several shops at once.
- ConstraintLow-memory iPhones are part of the buyer base, so the phone's memory sets the budget, not a development laptop.
The architecture, before and after
Switch between the two. The back end and the admin dashboard did not move. The storefront between the shopper's phone and the API was replaced, and the work of building a page moved from the phone to the server.
- What hurt in this state
- Built in this engagement
Three decisions, and what we turned down
The stack is the easy part to copy. The choices behind it are what made it work, so each one names what it replaced and what it cost.
Build the first screen on the server, not on the phone
Turned down
Keep rendering every page in the browser
The phone downloads and runs the whole bundle before a shopper sees a shop, which is both the slow first load and the memory pressure the brief named.
Turned down
Pre-render pages once at build time
Shops and stock change during the day in a same-day marketplace, so a page built ahead of time goes stale before a shopper opens it.
Chosen
Server rendering through ReactPWA, with skeleton loading
The first screen arrives as HTML a search engine can read, and skeletons hold the layout while the rest of the data loads.
What it cost: Every page view now costs server work, and every component has to render the same way on the server and in the browser, which rules out code that assumes a window exists.
Replace the storefront, keep the back end
Turned down
Rebuild the whole platform
The API and the admin dashboard worked. Rebuilding them would have put working parts at risk and delayed the fix shoppers needed.
Chosen
A new storefront built to the existing Loopback.io API
The problem was in the storefront, so the change stayed there, and the people running shops kept the dashboard they knew.
What it cost: The storefront had to take the API as it was. Where a response held more than a phone should keep, the storefront had to cope with it rather than change it.
One state tree, and render only what is on screen
Turned down
Hold the catalog in each component's own state
Copies of the same products spread across components add up on a phone with little memory, and each copy is one more place for a leak.
Chosen
MobX State Tree for state, off-screen components out of the render path
One typed store gives predictable updates to a cart that spans shops, and a long list keeps only what the shopper can see rendered.
What it cost: Items that scroll back into view are rendered again, so a fast scroll can show placeholders for a moment, and every store model needs a shape that works on the server and on the phone.
How it was delivered
The first screen first, then the state and the cart, then the memory work that only shows up on real devices.
The first screen
React with server rendering through ReactPWA, and skeleton loading while data arrives.
State and the cart
MobX State Tree for application state, and a cart and checkout that hold items from several shops.
Memory on iPhones
JavaScript bundles debugged against iOS Safari's memory limits, off-screen components taken out of the render path, and leaks on long product lists traced.
Who owns what now
The storefront runs against the client's existing API, and the back end and admin dashboard stayed with the client's own team.
Results
What changed for the people using the storefront. None of it was measured as a number.
- Shoppers can fill one cart from several local shops and check out once.
- Shoppers on low-memory phones get a storefront that works well even on older or less powerful phones.
- The client's team kept its back end and admin dashboard, so nothing changed for the people running shops.
Load times, memory use and crash rates were not timed before and after, so no speed figure is shown.
What the problem may be costing you
We do not publish what an engagement costs, and this engagement measured no speed figure. So this works only from your side: how many orders a storefront that fails on iPhones may be losing you each month.
Revenue at risk per month
Enter your figures
- Sessions lost per month
- Enter your figures
- Orders at risk per month
- Enter your figures
- Revenue at risk per year
- Enter your figures
A lost session on a same-day order is usually an order placed somewhere else.
What went wrong, and what we would change
iPhones ran out of memory before anything else broke
iPhones ran out of memory. Safari on iOS gives a page less memory than most Android browsers, and the app crashed at first. It was fixed by debugging and cutting down the JavaScript bundles. Memory limits only show up on a real device with a real catalog. Next time, the lowest-memory iPhone in the buyer base goes on the desk in the first week, loaded with a full catalog, not a sample one.
Long product lists leaked memory
Long product lists leaked memory, most of all on low-memory devices. A leak on a list grows with every scroll, so it passes a short test and fails a real shopping session. What we would change: a scripted long scroll with a memory check, run on every build, so a leak fails the build rather than a shopper's phone.
Will this work for your storefront?
It fits when
- Your back end is sound and the slowness or the crashes are in the browser.
- Your catalog runs to thousands of items and shoppers browse long lists on their phones.
- A real share of your buyers are on iPhones, and some of those phones are old.
Look elsewhere when
- Your API is slow. Rendering on the server moves the wait, it does not remove it.
- Your catalog is small and changes rarely. Static pages may be enough without a render server.
- Your buyers are mostly on desktop. The memory limits in this case study are a phone problem.
Check it yourself this week
If an old iPhone struggles on your longest list, this case study is about you. Put your iPhone sessions into the calculator above.
- Find the oldest iPhone your analytics show in real use, and open your storefront on it.
- Scroll your longest product or shop list to the end and back, then note whether the page reloads or goes blank.
- In your analytics, compare the share of iPhone sessions that end on the first page with the same share on desktop.
The stack, by layer
- StorefrontReact with server-side rendering through ReactPWA
- StateMobX State Tree
- RenderingSkeleton loading, off-screen components taken out of the render path
- CommerceA cart and checkout that hold items from several shops
- Kept in placeThe client's Loopback.io back end and React-Admin dashboard
How each figure was measured
- Crashes on iPhones with limited memory, traced to bundle size and fixed before launch.
- The catalog the storefront had to carry: thousands of products across thousands of shops.
- First load and time to interactive, moved to the server with server-side rendering.
- Memory held by long lists, reduced with offscreen rendering and skeletons.
- The existing Loopback API and React-Admin dashboard, kept as they were.
- A cart that holds items from several shops at once.
- Shops on the platform, as reported by the client.
Tell us which phones your shoppers use, and where your storefront slows down.
Send the page that struggles and the oldest phone you see in your analytics. A software engineer reads it and replies with an honest answer, including when the fix belongs in the back end instead.