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.
In this article, we will explain some aspects of how we can achieve route-based code splitting along with some code examples.
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>
Show me the code
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.
Want to see more?
I mainly write about real tech topics I face in my everyday life as a Frontend Developer. If this appeals to you then feel free to follow me on Twitter: https://twitter.com/cmacdonnacha
Bye for now. ๐