Thousands of shops on one storefront, and iPhones that ran out of memory.

A hyperlocal marketplace sells same-day delivery and in-store pickup from local shops. Its back end worked and its storefront did not. We rebuilt the storefront with server rendering and a smaller memory footprint, and left the back end and admin dashboard as they were.
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

The outcome in five lines
MeasureBeforeAfterEvidence
Crashes on iPhones with limited memory1the app crashed on some iPhonesbundles debugged and cut down to prevent the crashesDescribed
Thousands of products and shops2the storefront struggled to handle themhandled without slowing the pagesDescribed
First load and time to interactive3too slow, the client's own briefpages rendered on the server, with skeletons while data arrivesDescribed
Memory on long product lists4leaks when many products were shownoff-screen components kept out of the render path, leaks tracedDescribed
Back end and admin dashboard5a Loopback.io API and a React-Admin dashboardthe same two, unchanged, with the new storefront built to themDescribed

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.

Before. A working back end, and a storefront that could not hold the catalog on a low-memory phone.
The architecture, before and after: BeforeBefore: A working back end, and a storefront that could not hold the catalog on a low-memory phone. Components: Shoppers' phones, Old storefront, Loopback.io API, Shops and products, React-Admin. Shoppers' phones connects to Old storefront, dashed. Old storefront connects to Loopback.io API. React-Admin connects to Loopback.io API. Loopback.io API connects to Shops and products.Shoppers' phonessome iPhones low on RAMOld storefrontthe original storefrontLoopback.io APIthe client's back endShops and productsin the thousandsReact-Adminthe client's dashboardSome iPhones ran out ofmemory and the appcrashed.Thousands of productsand shops slowed thepages.
After. The server builds the first screen, one store holds the state, and the back end is unchanged.
The architecture, before and after: AfterAfter: The server builds the first screen, one store holds the state, and the back end is unchanged. Components: Shoppers' phones, React storefront, Loopback.io API, Shops and products, Lean rendering, MobX State Tree, React-Admin, Multi-shop cart. Shoppers' phones connects to React storefront. React storefront connects to Lean rendering. React storefront connects to MobX State Tree. MobX State Tree connects to Multi-shop cart. React storefront connects to Loopback.io API. React-Admin connects to Loopback.io API. Loopback.io API connects to Shops and products.Shoppers' phoneslow-memory iPhones tooReact storefrontSSR through ReactPWALoopback.io APIthe client's back endShops and productsin the thousandsLean renderingskeletons, offscreenMobX State Treeone store for app stateReact-Adminthe client's dashboardMulti-shop cartitems from many shops
  • 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.

  1. 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.

  2. 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.

  3. 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.

  1. The first screen

    React with server rendering through ReactPWA, and skeleton loading while data arrives.

  2. State and the cart

    MobX State Tree for application state, and a cart and checkout that hold items from several shops.

  3. 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.

Your storefront today

an assumption, change ityours to enter

Assumptions to replace with your analytics

This estimates what the problem may cost you, not what we achieved. No figure from this engagement is used, because none was measured. The two percentages are assumptions, so replace them with the numbers your analytics hold for iPhone sessions.

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.

  1. Find the oldest iPhone your analytics show in real use, and open your storefront on it.
  2. Scroll your longest product or shop list to the end and back, then note whether the page reloads or goes blank.
  3. 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

  1. Crashes on iPhones with limited memory, traced to bundle size and fixed before launch.
  2. The catalog the storefront had to carry: thousands of products across thousands of shops.
  3. First load and time to interactive, moved to the server with server-side rendering.
  4. Memory held by long lists, reduced with offscreen rendering and skeletons.
  5. The existing Loopback API and React-Admin dashboard, kept as they were.
  6. A cart that holds items from several shops at once.
  7. 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.