Keeping a React SPA Reliable Across Daily Deployments
Michael Murray
Morley Zhi
When we moved our React app from one large JavaScript bundle to many code-split bundles, we ended up with the classic user complaint: “Your site is loading a blank screen!”
We use error reporting to investigate issues like this, so the triage engineer opened Sentry and quickly found the classic import error:
How Playground works
Like many web apps, Playground is a large single-page application. And when I say large, I mean LARGE. Playground runs thousands of daycare centers around the world, users can do a ton of things on the app: manage students and their guardians, post to feeds, handle billing, manage their CRM pipelines, create and sign documents, and so on and so on.
All this adds up to a lot of routes (437) and a LOT of Javascript (23MB)
Asking customers to download 24MB of JavaScript upfront isn’t the experience we want for Playground users and downloading 6-7MB of gzipped JavaScript isn’t much better. So instead, we use route-level lazy loading. When an administrator — let’s call her Alice — loads the Playground homepage, she only downloads the JavaScript needed to render that route.
Deploying new SPA versions, creates a race between the latest build and the users still running an older version. Because our single-page application uses route-level lazy loading, a user might not request a JavaScript chunk until long after their session begins. If we deploy again before that happens, the chunk their app expects may already be gone. A routine page transition then becomes a 404 — and a broken experience.
Steps to recreate the bug
An engineer deploys a new build: let’s call it Version A
Alice loads Version A’s home screen
Another engineer makes a new build, Version B
Version A’s Javascript assets stop becoming available
Alice tries to load Version A’s student screen
Version A’s student screen attempts to load some JS assets, but gets back a 404 instead
Version A crashes, Alice is sad and we are sad
Our challenge was to preserve the performance benefits of lazy loading while maintaining Playground's ability to ship fast. Like up to 30 times a day!
Our solution
The answer to the problem is to preload all the routes that would be available after we load the first route. This gives us the benefit of the speed of lazy loaded routes with the durability of a mono bundle. Which is a similar strategy Linear uses as well. Once the app is preloaded, navigating through the app becomes instant.
Side note: It’s surprising that platforms like Cloudflare and Netlify don’t make this easier. Only one app version is active, so assets from previous builds disappear after a deploy.
Implementation
Inspired by react-lazy-with-preload, we extended the React.lazy api to include a preload() method:
We then define our routes in one place and preload them in two stages: first the current route, then every remaining route in the background.
The following example is simplified for clarity:
As a result of the solution we found, when Alice loads Playground now:
The homepage only loads home route assets, so it appears as soon as possible
After the homepage loads, every other route loads in the background
Subsequent routes load from browser cache
Playground engineers can continue to ship continuously.
After deploying preloading, the Sentry errors significantly decreased.
Playground’s core engineering tenet is simplicity and speed. We love a solution that’s straightforward to understand, easy to build, and simple to manage. By that rubric, lazy preloading has been a success.
© 2026 Carline Inc. All rights reserved.