Recently I've got back to creating side projects, which is actually quite refreshing, albeit, with all the technology options, it can be quite time consuming and daunting to just get started. Anyhow, as I've been working on my own side project (I will post it to YouTube at some point), I've become much more interested in performance, and as I was optimising my application (which I tend to do after, although if I can get some quick wins along the way, great) I thought the first easy win was to just decrease the bundle size.
So how do we go about decreasing the bundle size? Well, the first quick win, if you're using client-side rendering that is, and of course React Router (this does not work with SSR), is to simply split your routes--basically get rid of anything that isn't directly used on the page the user is visiting.
Here's an example...
A user visits your homepage and let's say the user downloads your initial bundle which isn't cached at around 320kb, well what is in that bundle for it to be 320kb? Turns out you have 5 additional 5 routes in there (that aren't being directly used)...so what do we do with those additional 5-page routes that aren't currently being used on the homepage? The answer is simple, we get rid of them! ποΈ
Karl, but what exactly is code splitting?
Code splitting is basically the removal of code that the user does not need right now. Prime example...the user visits your website and the browser downloads the JavaScipt bundle file which includes the following:
Home, sign in, sign up, faqs, docs
It's clear that we don't need sign in, sign up, faqs, docs
right now, so we can just remove them from the initial bundle, lowering our bundle size!
Lowering our bundle size is great because it means our website will load faster and if you're not caching the JS file downloads, you're probably saving your users from having to pay extra for data if they're on mobile!
Only ship the minimal amount of code to the browser to render the page the user is on!
Okay, that's all well and good, but how do we do that?
Enter dynamic routes, React Suspense and React lazy!
Let's say you have a basic React router setup like so:
<Router>
<Switch>
<Route path="/sign-in">
<SignIn />
</Route>
<Route path="/sign-up">
<SignUp />
</Route>
<Route exact path="/">
<Home />
</Route>
</Switch>
</Router>
This looks normal, right? Well, it is. Unfortunately, if you look in the dev tools and take a look for your bundle file (take a look, I'm sure you'll be able to find it!), do a search for the contents that are within sign in or sign up. If you have 'username', or 'email address' within the sign in or sign up pages, search for it within your bundle, and you will see it's in there!
We don't want it to be in there because we're not using it right now!
Of course, don't forget your imports:
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import SignIn from './pages/sign-in';
import SignUp from './pages/sign-up';
import Home from './pages/home';
Time to start cleaning this up & to use Dynamic Imports
Even though we just imported our pages (components) above, we want to restructure them to be the following (removing our regular imports and brining in Suspense!):
import React, { Suspense } from 'react';
const Home = React.lazy(() => import('./pages/home'));
const SignIn = React.lazy(() => import('./pages/sign-in'));
const SignUp = React.lazy(() => import('./pages/sign-up'));
What we just did is essentially we converted our regular imports into dynamic imports, which means we have dynamically converted our imports and they're ready to be used as a React component (using React.lazy), there is one caveat...
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component.
The lazy component should then be rendered inside a Suspense component, which allows us to show some fallback content (such as a loading indicator) while weβre waiting for the lazy component to load.
You can read more about code splitting from the React docs here. Basically what a dynamic is saying (in layman terms) is...when you're ready for me to be used, call me!
As the docs state, if we want to use React.lazy
, we must use Suspense! Let's go ahead and change our code a little and basically just wrap our React router routes with Suspense!
<React.Suspense fallback={<p>Loading...</p>}>
<Router>
<Switch>
<Route path="/sign-in">
<SignIn />
</Route>
<Route path="/sign-up">
<SignUp />
</Route>
<Route exact path="/">
<Home />
</Route>
</Switch>
</Router>
</React.Suspense>
As you can see I've provided a fallback of <p>Loading...</p>
, that's just me being lazy (no pun intended). To quote the React docs again...
The fallback prop accepts any React elements that you want to render while waiting for the component to load. You can place the Suspense component anywhere above the lazy component. You can even wrap multiple lazy components with a single Suspense component.
Lastly, it's now time to change our Route
to something a little different. Remember above when I spoke about calling the dynamic import so we can use it? Well, let's do that!
Change the above code to match the following and you're good to go!
<React.Suspense fallback={<p>Loading...</p>}>
<Router>
<Switch>
<Route path="/sign-in" render={() => <SignIn />} />
<Route path="/sign-up" render={() => <SignUp />} />
<Route exact path="/" render={() => <Home />} />
</Switch>
</Router>
</React.Suspense>
And that's pretty much it, go ahead and check that pesky bundle file now and you'll see there's no sign in
or sign up
contents in there, but when you visit the sign in
or sign up
pages, you'll see in the network tab that it pulls through the contents in an additional JavaScript file!
A quick few notes...this method doesn't work with server-side rendering (I don't believe), but React Loadable does, so check that out!
Furthermore, you have to make sure you have exported your components as a default export! To quote the docs...
React.lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you donβt pull in unused components.
If youβre using Create React App, Next.js, Gatsby, or a similar tool, you will have a Webpack setup out of the box to bundle your app.
That's code splitting π
There's much more you can do for performance increases and I strongly recommend taking a look and doing your own research. Not only is it really interesting but in the world of JavaScript, it's something we should take seriously!
For more advanced performance tips, consider following Ivan Akulov on Twitter, he's a performance genius and I basically steal all his tips π
I have created a more in-depth tutorial around this on my YouTube channel - considering giving it a watch, or simply skipping to the end if you just want the solution!
--
π₯ If you enjoyed this post, please consider subscribing to my YouTube channel where I post React, JavaScript, GraphQL videos, and of course quick tips! I'm also on Twitter - feel free to @ me with any questions!
Top comments (4)
Great guide! Just covered this on a Udemy course so this was a nice refresher
That's awesome dude, if you have any questions, just let me know!
Thanks for this post, but I want to ask you what is the difference between render prop and component prop in react router
For simple usage
component
is neat butrender
is more flexible as you can declare a component inline, pass props or modifiy an existing component etccomponent={Signin}
render={()=> <Signin {...anyProps} customProp="foo" />}