Frontend Performance Optimization: React Code Splitting and Bundle Size
Psst: we are hiring remote frontend developers and backend developers.
Frontend performance is important. Not only is this good for your users, it is also crucial if run an ecommerce, want to improve ecommerce SEO, and is actually really important for our planet as well. Use code splitting and reduce the size of your bundles. Measure and optimise our frontend performance continuously. Let's go millisecond hunting.
Why JavaScript?
Why do we provide JavaScript to download and execute for our users at all? This is a totally valid question.
If you are running a React application, it is actually perfectly fine to just render it on the server, and then allow the user to navigate JavaScript-free. Sure, this will not give the users the benefit of a client-side rendered page, and if they navigate around on your site, a new page will be rendered on the server and served to the user every time. What is the cost of this in bytes?
Let’s check out our own site as an example, where we start on the Crystallize frontpage and make 2 navigations afterwards.
JavaScript turned on:
- / - 36 req, 860kb
- /blog - 16 req. 160kb
- /blog/frontend-performance-measuring-kpis - 22 req, 577kb
- /blog/seo-for-react-ecommerce-spa - 19 req, 688kb
- Total: 93 req. 2285 kb
JavaScript turned off:
- / - 13 req, 308kb
- /blog - 11 req. 53kb
- /blog/frontend-performance-measuring-kpis - 16 req, 398kb
- /blog/seo-for-react-ecommerce-spa - 16 req. 674kb
- Total: 56 req. 1433 kb
With JS turned off we save 852kb (~37%) in bytes downloaded, and the speed is magnificent. What are we missing? In short: fonts, images and tracking. We use lazy-loading on many of our images, so you are simply not getting those. Around 80kb of the JS are external JavaScript, like font loading and GTM. 70kb are the actual downloaded fonts. You are also not getting the benefit of caching of pages, so if you revisit any of the visited pages, you end up downloading the whole page again not just the data for it. Additionally, you are not getting the interactional components that we embed on the page.
You are also visiting the server every time, so if the server has a lot to do, the speed would slow down. With JS turned on you would (on our site) just query the GraphQL PIM API once, and it is usually a super fast API.
In some cases, to not ship our JS-bundles to the user is just fine. If it benefits the user and you still are satisfied with their experience on your site. Do it.
Split and Reduce your Bundles
We have established that the JS bundles on your site have a massive impact on the downloaded bytes. But let’s be honest, most of us will still end up shipping JS to our users. Let’s make sure we do it the right way.
Firstly:
Get an overview of what you are shipping to your users. If you are using source maps, you can get a very detailed overview of your bundle contents by using source-maps-explorer.
Get rid of anything that takes up too much space. See if there are more lightweight alternatives for the libraries you are using. Using moment.js? Try out date-fns. Using lodash? Make sure you import only the individual parts that you actually use:
Do
import uniq from 'lodash/uniq';uniq([])Don’timport _ from 'lodash';_.uniq([])
Secondly:
You want to make sure that the users are getting the JS that they need for the page they are on, and nothing else. No reason to fetch JS for all the possible pages and components on the page right away. This can be deferred until they are actually needed. This is called code splitting and is an essential part of frontend performance.
Code Splitting using React.lazy
React.Lazy works by not downloading the JS a component needs before it is mounted on the page. It is a perfect solution when you want to take parts of your application out of your bundle and defer the loading to when it actually needs to happen. This is essentially how you do it (taken from the React code-splitting guide)
const BigComponentThatIWantToSplitOut = React.lazy(() => import('./BigComponent));function MyComponent() { return ( <div> <React.Suspense fallback={<div>Loading big component…</div>}> <BigComponentThatIWantToSplitOut /> </React.Suspense> </div> );}React.Suspense is also used here to display a fallback message that will be visible during the download and execution of “BigComponent”.
Beware: React.Lazy does not currently work on the server, so if you want to have this on a server-side rendered application, which we highly recommend, you should try out Next.js (which comes with code-splitting on route level out-of-the-box), or react-loadable.