Hey there! Full disclaimer: this is my first post so I am completely open to feedback. If there's something you like or something you don't like about the post, feel free to let me know. Now, onto what you actually came here for: code splitting. Note: all the code referenced in this post is in this Github repo here: https://github.com/ChasseRush/code-splitting-example. Feel free to take a look at it and follow along.
What is code splitting?
Well, I'm glad you asked. Code splitting is a way of removing code from your main bundle, the bundle that is sent to the user when the user loads your app/page. This removed code is instead put into a separate bundle, which will be sent over the network sometime later. You're essentially saying "Hey, users don't need this right now, but they may later, so hold off on loading this code until then." Code splitting effectively can result in some huge performance boosts for large apps.
So, when should I use code splitting?
Another great question my imaginary reader. There are two obvious ways to apply code splitting: when users won't need some code until they go to a certain URL, like example.com/settings
, and when users have to have some kind of interaction with your app before needing some code, like clicking a button to open a modal. Following these general guidelines can get you started on pinpointing key parts of your app that could use some code splitting attention (here's a hint, if you use react-router
, look at where you declare your routes and you can generally start from there).
That sounds great, but how do I do it?
Another fantastic question, and that's what I'm here for. Now, let's say we're in a theoretical environment where we have an app, and that app has a feedback page at myapp.com/feedback
, and we're asking for a whole lot of feedback. We have every component imaginable in there, even a RichText
editor because we're nice and we want to allow users to customize their feedback and not be limited to just a plain textbox. And then if they try to leave before submitting feedback, we give them a modal asking if they're sure they don't want to give us feedback and then... I think you have the idea. So, we have three main routes, let's call them /app
, /settings
, and /feedback
.
Let's say that when the user opens our app, we go straight to the /app
page. As a result, it may not be worth the trouble of code splitting that out. Sure, users may go straight to /settings
if they type in the URL directly, but the odds of that are pretty slim and our /app
component isn't that huge. And our /settings
page is pretty small with a few buttons, so our main issue is /feedback
. Right now, our App.js
file may look something like this:
"use-es6"
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { Route } from "react-router-dom";
import AppContainer from "./Containers/AppContainer.js";
import SettingsContainer from "./Containers/SettingsContainer";
import FeedbackContainer from "./Containers/FeedbackContainer";
function App() {
return (
<BrowserRouter>
<div className="content">
<Route path="/app" exact component={AppContainer} />
<Route path="/settings" exact component={SettingsContainer} />
<Route path="/feedback" exact component={FeedbackContainer} />
</div>
</BrowserRouter>
);
}
export default App;
Pretty basic, right. Now, there are two ways we can begin code splitting this massive FeedbackContainer
. We can go with React's builtin approach or the webpack approach. Either works fine, it's just a matter of preference.
We'll start with the React approach. React actually has this builtin function called React.lazy()
, that when paired with their Suspense
component, allows us to dynamically import our component (i.e code splitting). One thing to point out with React.lazy()
however, is that it only supports default
exports. So let's take a look at how we set up React.lazy()
. It's pretty simple, all we have to do is pass in a function that calls a dynamic import, in our case the FeedbackContainer
, like so:
const AsyncFeedbackContainer = React.lazy(() =>
import("./Containers/FeedbackContainer")
);
And now we have our async component, just by using React.lazy()
and passing in the dynamic import statement. We're just about done code splitting this out! Now, we have to swap our AsyncFeedbackContainer
in place of FeedbackContainer
in <Route path="/feedback" exact component={AsyncFeedbackContainer} />
and wrap that Route
with a React.Suspense
component. This suspense component is very important as it allows us to show some kind of loading component while we're waiting for the bundle to load. This can be crucial when we're code splitting out a huge component. As a result, the Suspense
component requires the fallback prop, which is what will be rendered when the bundle is still loading. This can be anything from a spinner, to some bouncing dots, to a div
that just says "Loading...". I personally like spinners, so I'm going to just import my spinner and pass it into the fallback
prop of the Suspense
component. Now we should end up with something like this:
"use-es6";
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { Route } from "react-router-dom";
import AppContainer from "./Containers/AppContainer.js";
import SettingsContainer from "./Containers/SettingsContainer";
import Spinner from "./Components/Spinner";
import "./App.css";
const AsyncFeedbackContainer = React.lazy(() =>
import("./Containers/FeedbackContainer")
);
function App() {
return (
<BrowserRouter>
<div className="content">
<Route path="/app" exact component={AppContainer} />
<Route path="/settings" exact component={SettingsContainer} />
<React.Suspense fallback={Spinner}>
<Route path="/feedback" exact component={AsyncFeedbackContainer} />
</React.Suspense>
</div>
</BrowserRouter>
);
}
export default App;
And voila, our giant feedback component has been code split out of our main bundle. To see this, all you have to do is go to your network tab, look at the JS that's loaded for the '/app' route, and then do the same for the '/feedback' route. You'll notice that you have another bundle loaded on the '/feedback' route, and that's our code splitted FeedbackContainer
! That about does it for the React approach, now for the webpack approach!
Code Splitting with webpack
Maybe you don't like using React.Suspense
, or maybe you just want to see what other options you have for code splitting. Well, have no fear, because webpack also provides you with a way to code split your app. To start, you'll want to make sure that in your webpack.config.js
file, you have
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
...
...
},
Now, you'll also want to have a component that will allow you to dynamically import the module, while also having a way to show some kind of loading indicator while the component is being loaded. An error indicator would be nice as well. I'd show you how to do it, but there's already a really good package for this called react-loadable
that has all that done for you. I'd recommend installing it by doing npm install react-loadable
.
Using the Loadable
component is super easy, all you really have to do is pass in an object with a loader
, which would just be your dynamic import, and a loading
indicator, like our Spinner.
Now, let's go back to our theoretical example. Let's say our app grows, and so does our SettingsContainer
component. Maybe it has some simple stuff in it, like some buttons or some text fields, but we also have some huge components in it. Let's say we have one component that is enormous, it has an emoji picker in it so the user can select their favorite emoji and an option to select your favorite Google font, with every Google font imported. But, the user can only get to it by clicking on a button, maybe something like Click here to set your favorites
. So, apart from this enormous component, our Settings
component is pretty small. It doesn't really make sense to split the entire settings component, just this one huge component inside of our SettingsContainer
that we'll call SuperHugeComponent
. This is where component-based splitting comes into play.
But what is component-based splitting?
Component-based splitting is exactly what it sounds like: you're code splitting out a component because it won't be used until the user has some sort of interaction with your app. In our case, the user has to click a button in order to even see the SuperHugeComponent
so this is a perfect opportunity for component-based splitting. To do this, we'll use webpack's magic comments and our newly added Loadable
component.
So, let's say our Settings component looks like this:
"use-es6";
import React, { useState } from "react";
import SuperHugeComponent from "../Components/SuperHugeComponent";
export default function SettingsContainer() {
const [shouldShowHugeComponent, updateShouldShow] = useState(false);
return (
<div className="main-section">
<h2>I'm the settings part of this pretty cool app</h2>
<h2>My bundle size is pretty big though, like what /app said</h2>
<h2>
I also have this really weird HugeComponent that's slowing me down
</h2>
<button
className="button"
onClick={() => updateShouldShow(!shouldShowHugeComponent)}
>
Click me to set your favorites
</button>
{shouldShowHugeComponent && <SuperHugeComponent />}
</div>
);
}
As you can see, SuperHugeComponent
is not even used until the button is clicked. So, now let's make it an async component using our Loadable
component and webpack's magic comments. First, import Loadable using import Loadable from "react-loadable"
and since it takes a loading
component, we should import our Spinner
. Now, we just make our async component like so
const AsyncSuperHugeComponent = Loadable({
loader: () => import("../Components/SuperHugeComponent"),
loading: Spinner,
});
And now we have our async component for SuperHugeComponent
. But, what about the magic comments? Webpack's magic comments allow you to do a number of great things with our dynamic imports, but for now, we'll start with /* webpackChunkName: 'SuperHugeComponent' */
. This gives our separate bundle a specified name: SuperHugeComponent. I could go into all of the other webpack magic comments, but this is already getting pretty long, so let me know if you'd be interested in a separate post going over those. So, adding this comment, our async component becomes
const AsyncSuperHugeComponent = Loadable({
loader: () => import(/* webpackChunkName: 'SuperHugeComponent' */ "../Components/SuperHugeComponent"),
loading: Spinner,
});
Now just like with React.lazy
, we swap this in place of SuperHugeComponent
, remove its import, and we're good to go! Now our code should look like this.
"use-es6";
import React, { useState } from "react";
import Loadable from "react-loadable";
import Spinner from "../Components/Spinner";
const AsyncSuperHugeComponent = Loadable({
loader: () =>
import(
/* webpackChunkName: 'SuperHugeComponent' */ "../Components/SuperHugeComponent"
),
loading: Spinner,
});
export default function SettingsContainer() {
const [shouldShowHugeComponent, updateShouldShow] = useState(false);
return (
<div className="main-section">
<h2>I'm the settings part of this pretty cool app</h2>
<h2>My bundle size is pretty big though, like what /app said</h2>
<h2>
I also have this really weird HugeComponent that's slowing me down
</h2>
<button
className="button"
onClick={() => updateShouldShow(!shouldShowHugeComponent)}
>
Click me to set your favorites
</button>
{shouldShowHugeComponent && <AsyncSuperHugeComponent />}
</div>
);
}
Again, if you go to the '/settings' page and take a look at the JS filter in the network tab in dev tools, you'll see that when you click the button, we import our new module named SuperHugeComponent.chunk.js
, which is the result of using the webpackChunkName
magic comment. Now we've successfully code split one of our routes using React.lazy()
and one of our components using webpack and react-loadable
!
Wrapping up
I hope you've learned something from this code splitting introduction! I just want to point out that the best forms of code splitting usually involve some combination of both route-based splitting and component-based splitting. Route-based splitting can only take you so far, while combining route-based splitting with component-based splitting can help you unlock your true web performance potential. Like I said before, this is my first post so give me any feedback you might have. If you'd like for me to follow up with a post on making a basic component for dynamic imports or one about webpack magic comments feel free to let me know! Again, here's the Github repo so you can reference it later on: https://github.com/ChasseRush/code-splitting-example.
Top comments (0)