Keeping an 👁 on Web Performance
Why speed on mobile matters
PS: originally posted on medium in Dec, 2018.
90% of our users are mobile and I am sure yours are too. They carry around your business in their pockets and we carry their hopes and dreams :)
Now, as many as 85% these users are using low-end mobile devices
This represents a new reality for me and my team working here at shaadi.com. I work on a product called sangam.com and every decision we plan to ship to production needs to be based on a set of constraints.
Things that work on the desktop will not work right on mobile
Don’t keep your users waiting
A recent Google study indicated that 53% of people will abandon a mobile site if it takes more than three seconds to load. Still, our site used to take a lot more than that (~20secs).
This is what lighthouse score was before we started working on performance:
Let’s have a closer look at those numbers
main bundle map using webpack-bundle-analyzer before code-splittingjs-expensive
It soon became evident to us that our product was suffering badly due to performance issues and something had to be done to solve this problem.
Here’s what we fixed
Initially, we started splitting our components based on routes using a very powerful react library react-loadable that created an individual chunk for each defined route but the main problem isn’t solved, our main bundle was still heavy. Most of the actions, reducers, decorators, and API controllers were still part of the main bundle which was only needed for specific routes/components.
Code splitting can be fun
We also realised we could split our actions, decorators and API controllers based on their routes/components. So we attacked this problem with a solution which was very similar to the one Dan Abramov suggested for code splitting in Redux in one of his GitHub gists but instead of splitting reducers, we split away actions, decorators and API controllers.
Keep a look out for a future post on code splitting redux
After all that hard work, our chunks started looking like this.
And finally, this is the result of webpack-bundle-analyzer.
main bundle map using webpack-bundle-analyzer after code-splitting
It has a main bundle which is less than 200 KB and few other route-based chunks (20–30 KB each) which we’re loading based on routes.
[UPDATE] Create React App V2
So in summary what we used are these approaches and tools
Prefetching for the win 🙌
To solve this problem, the chunks would need to be prefetched & pre-cached and that’s how they would be already available for other routes as soon as the user tries to change the route.
Finally, we thought of using service workers.
We chose Workbox to implement different caching strategies such as stale-while-revalidate, cache first. Workbox provided us with a simple API to implement these strategies. Along with precaching critical bundles we also implemented runtime caching when a user visits non-critical pages.
Stale assets cache are automatically clean up on each release and each cache is versioned to avoid cache collision.
All of this has significantly reduced our time-to-interactive and given us the boost we wanted to achieve. We have a long road ahead of course but we know we are prepared for the journey, and I hope this will help you navigate performance roadblocks on your project and keep your app under budget too :)
References to tools and strategies used
- Service Workers
- Web App Manifest
- Dynamic Import
- Code Splitting
- Can You Afford It?: Real-world Web Performance Budgets
- PRPL Pattern
- User-centric Performance Metrics
- Redux modules and code-splitting
- Dan Abramov’s Splitting Approach