Imagine you've been to a café and order a cup of coffee ☕. Once the bill 🧾 arrives, it isn't only for the coffee you ordered, but all the menu items available in the café. How would you feel? Shocking right!! It would be unfair to pay for all the items you haven't even ordered. Without arguing, you paid the bill, and never returned to this café.
Well, that was just an analogy. Let's relate it with our web platform, build with an enormous JavaScript bundle.
Here, our user is the customer, and we(developers) are the café owners. If our user has only requested the registration/signup
form, would you also send down the rest of the JavaScript bundle(the bill) responsible for the rest of the pages, carrying huge map or date libraries? How would your user feel? Most likely upset or might not come to your website again, right?
The obvious reason is that their first-page load experience would be slow, and the page might take more time to be interactive (TTI & FID). The browser will keep itself busy parsing the JavaScript, while our users stare at a blank screen with a sad face ☹️.
And the sad thing is that our poor user has no clue that it was us, the developers, who could have been more responsible while sending the full JavaScript bundle down to them in one go.
Welcome to the world of code-splitting where you can lazy-load (dynamically) your JavaScript bundle dynamically, only when the user requested it. The bill you hand over to your user is exactly what they have eaten 😄.
Route-based splitting
All modern JavaScript bundlers, like Webpack. Rollup, and parcel, supports code-splitting feature out of the box. These bundlers can create multiple bundles that can be dynamically loaded at run time, thus improving the web performance for your users.
Splitting your JavaScript bundle based on the routes/pages
in your app is called route-based code splitting. For example, if you have login
and a home page, you would more likely be splitting the bundle based on these routes. And only send login
page JavaScript when the page loads.
NextJS provides this route-based splitting feature out of the box. And if you're using React Router, React-lazy is your best bet.
Component-based splitting
With route-based splitting, we've made our users happy. It's time we take an extra step and implement component-based splitting. Let's understand this with an example, followed by a coding exercise to strengthen our concepts. Before you realize it, it will become a piece of cake for you 🍰.
Imagine you're building a page to show a rental property. There's a button on this page that opens up a full-page map to show its address. This map component carries a complex functionality and has contributed to a large amount of JavaScript bundle.
import JSHeavyMapComponent from './js-heavy-map-component';
// Property page component
export default function Property() {
const [showMap, setShowMap] = useState(false);
return <>
<h1>Rental Property</h1>
<article>
<h2>Property description</h2>
{ showMap && <JSHeavyMapComponent /> }
<button onClick={() => setShowMap(true)}>
Show map
</button>
</article>
</>
}
Would you make this map component part of your initial property page (route-based) bundle? What if the user never clicks the button, and only views the property metadata? Wouldn't that be a waste of resources to parse all that extra JavaScript causing the slow page load time?
Yes, it would be unnecessary to send along all this heavy JavaScript bundle in this case. It might cause a heavy toll on mobile users where resources are limited compared to Desktop users.
This is where the component-based loading comes into the picture and mitigates these issues. With this approach, you can lazy-load the map component, and dynamically serve it when the user actually asks for it (click the button). This will make your property page lean, improving the overall page-load performance. You can put more effort, and download the component when the user is about to hover the button, saving you an extra microsecond there.
With the theory aside, we'll see how could you easily implement it in code using the dynamic import feature. We'll see two examples starting with React.lazy approach, and then approach to do the same in NextJS projects using dynamic import feature.
So, let's get started.
Lazy-loading via React.lazy
We need to use React.lazy along with Suspense to lazy-load our Map component dynamically.
// Change the old import to use React.lazy
const JSHeavyMapComponent = React.lazy(() =>
import("./js-heavy-map-component")
);
// Property page component
export default function Property() {
const [showMap, setShowMap] = useState(false);
return (
<>
<h1>Rental Property</h1>
<article>
<h2>Property description</h2>
{/* Wrap you dynamic component with Suspense */}
{showMap && (
<React.Suspense fallback={<p>Loading...</p>}>
<JSHeavyMapComponent />
</React.Suspense>
)}
<button onClick={() => setShowMap(true)}>Show map</button>
</article>
</>
);
}
So, with this change, when your property page loads, the browser will not load extra JavaScript for the map component. The loading will only happen when the user hits the Show map
button—great saving with just a couple of lines. Didn't I say that it would your piece of cake 😉? Here's the codesandbox demo. Download and run the app locally on your computer. Keep an eye on your network
tab when you hit the Show map
button. Here's your lazy-loading
in action.
Lazy-loading in NextJS
With NextJS, implementing the dynamic loading is as easy as ABC. Similar to React.lazy API, NextJS has an equivalent dynamic module, which also let you pass additional options for loading component.
import dynamic from "next/dynamic";
// Change the old import to use NextJS dynamic import
const JSHeavyMapComponent = dynamic(() => import("./js-heavy-map-component"));
// Property page component
export default function Property() {
const [showMap, setShowMap] = useState(false);
return (
<>
<h1>Rental Property</h1>
<article>
<h2>Property description</h2>
{showMap && <JSHeavyMapComponent />}
<button onClick={() => setShowMap(true)}>Show map</button>
</article>
</>
);
}
And here's the codesandbox demo for it if you want to play along.
Conclusion
My main goal through this article was not only to tell you how to implement the code-splitting (the official docs are great resources for this) but to help you think about why do we need it in the first place. Remember that your beautiful app is of no good use if it takes minutes to load. User is the focus, and we should be mindful of what we send across the wire to the clients. Don't make your users pay the cost for the additional JavaScript. And with such a great set of tooling at our disposal, there's no excuse to not do code-splitting. Your uses will thank you for this.
Think inclusive, think accessible.
Top comments (7)
I researched and saw same code examples everywhere. But this does not work for me. I'm using React v.18 and there is some error in React.lazy(). When i hit the lazy import, is consoles an error saying thenable.then is not a function.
I duged into it and found that there is an error in the React.lazy function in react v.18 atleast, that i'm using.
And also it does not support import of lazy component I'm bound to require it.
You might have passed something different than a dynamic import to
lazy
:How did you fix it?
@mrlectus See my comment above, maybe this helps!
Wowww man, it's just amazing... definitely gonna implement it.
I am glad that you liked it. Go for it. 👍
Great