Prologue
Earlier when we achieved our Super Saiyan power of Memory Management (quick promotion xD), we instantly became much more powerful than ever.
This helped us a lot on our quest for World Domination using JavaScript. However, we quickly realised that this power is incomplete. In order to make this power complete, we need more optimisation powers.
Thus, we're here to discuss one more such power - Route-Based Code Splitting.
Introduction
Since decades (if not years), it has been a general practice to bundle all the javascript files in a single main.bundle.js
file that will have all the codes of the project, including third party libraries.
Benefits of code splitting
For best practice, web developers code split large bundles into smaller ones because it enables them to lazy load files on demand and improves the performance of the React application.
This allows the browsers to only download those parts of the application which are necessary. When a new component/route is encountered, those parts can be downloaded on demand.
Lets understand with an example
Below given is an example of a production build for a React app:
From the image, we can see that the files are split into different chunks.
Main.[hash].chunk.css
represents all the CSS codes our application needs. Note that even if you write CSS in JavaScript using something like styled-components, it would still compile to CSSNumber.[hash].chunk.js
represents all the libraries used in our application. It’s essentially all the vendor codes imported from thenode_modules
folderMain.[hash].chunk.js
is all of our application files (Contact.js, About.js, etc.). It represents all the code we wrote for our React applicationRuntime-main.[hash].js
represents a small webpack runtime logic used to load and run our application. Its contents are in the build/index.html file by default.
Even though our production build is optimized, there is still room for improvement. Consider the following image:
From the image, we see that main.[hash].chunk.js
contains all of our application files and is 1000kB in size. We can also see that when a user visits the login page, the entire 1000kB chunk gets downloaded by the browser. This chunk contains codes the user may never need. Consequently, if the login page is 2kB, a user would have to load a 1000kB chunk to view a 2kB page.
What is code splitting
Code splitting is a technique used to optimize the loading performance of web apps by breaking down the bundled JavaScript files into smaller, more manageable chunks. By loading only the required code for a specific route or page, route-based code splitting significantly reduces the initial load time and improves the overall user experience.
Why Route Based code-splitting
When developing large-scale applications, loading all the JavaScript code upfront can lead to increased initial load times and negatively impact user experience. In contrast, route-based code splitting allows you to divide your application into smaller chunks based on different routes or features. Only the code relevant to the current route is loaded, resulting in faster loading times for the specific page and better overall application performance.
By using route-based code splitting, you can prioritize the most critical code for each route, optimizing the initial loading experience and reducing the time to interactive (TTI).
What do we need?
In order to actually implement route-based code splitting, we need to make use of two things:
Let's take a look at these in a bit more detail.
Dynamic import
Dynamic import is an ECMAScript feature that allows us to import modules on the fly. This is really powerful and unless you're unlucky enough to have to support IE, it can be used in all major browsers.
Here's what the syntax looks like:
import('./path-to-my-module.js');
import
will return a promise, so you would handle it just like any other promise within your app.
import('./carModule.js')
.then(module => {
module.startEngine();
})
.catch(error => {
console.error('Error loading carModule:', error);
});
// or you can use async/await
const carModule = await import('./carModule.js');
carModule.startEngine();
A great use case for this would be when you only make use of a heavy module in a specific part of your app.
<button onClick={onSortCarsClick}>Sort cars<button/>
function onSortCarsClick() {
// Load in the heavy module
const carModule = await import('./carModule.js');
carModule.sortCars();
}
React.lazy()
React.lazy()
is a function in React that enables you to perform "lazy" or "on-demand" loading of components. It ensures that the component will only be loaded when it's actually rendered.
Before React.lazy()
was introduced, you might have needed to set up a more complex build tooling configuration to achieve similar code splitting behavior. With React.lazy()
, this process is simplified and integrated directly into React's core.
const MyLazyComponent = React.lazy(() => import('./MyComponent'));
Suspense
Since the component is no longer statically imported, we need to display something while it's dynamically loading. For that, we use React's <Suspense>
boundary.
In the example below, you'll see we are displaying some fallback UI while the component is being loaded.
<Suspense fallback={<div>Loading...</div>}>
<MyLazyComponent/>
</Suspense>
Complete Example
Here's a full example of how we can use a combination of dynamic imports, React.lazy()
and React Router to achieve route-based code splitting.
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
// These components will only be loaded when they're actually rendered.
const Home = lazy(() => import('./components/Home'));
const About = lazy(() => import('./components/About'));
const Contact = lazy(() => import('./components/Contact'));
function App() {
return (
<Router>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/contact">
<Contact />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Suspense>
</Router>
);
}
export default App;
Code splitting using bundlers
Modern bundlers have built-in support for code splitting to enable efficient loading of modules. What often happens is when a bundler comes across a dynamic import within your app, it automatically creates a separate chunk (javascript file) which can be loaded later. This way it's not bundled within your main bundle file, and hence improving the initial load time of your app.
Final thoughts
As you can see, code splitting has the potential to give us big performance improvements. However, it's important not to become too obsessed with it as like most performance-related features, it also adds complexity, so only use it where it makes sense. This is why it's often a great first step to only use it for routes, and take it from there.
Most React apps will have their files “bundled” using tools like Webpack, Rollup or Browserify. Bundling is the process of following imported files and merging them into a single file: a “bundle”. This bundle can then be included on a webpage to load an entire app at once.
Conclusion
Bundling is great, but as your app grows, your bundle will grow too. Especially if you are including large third-party libraries. You need to keep an eye on the code you are including in your bundle so that you don’t accidentally make it so large that your app takes a long time to load.
To avoid winding up with a large bundle, it’s good to get ahead of the problem and start “splitting” your bundle. Code-Splitting is a feature supported by bundlers like Webpack, Rollup and Browserify (via factor-bundle) which can create multiple bundles that can be dynamically loaded at runtime.
While you don't reduce the overall amount of code in your app, you avoid loading code that the user may never need, and reduced the amount of code needed during the initial load.