Web Performance Optimisation Techniques II: Code Splitting


Code splitting is a technique where an application’s code is divided into smaller chunks or bundles, which are loaded on demand. Instead of loading the entire application at once, only the code needed for the current view is loaded. This improves performance and user experience by reducing the initial load time and optimising resource usage.
Why is Code Splitting Important?
Faster Initial Load Times: By loading only the code required for the initial page view, the application starts faster.
Enhanced User Experience: Users spend less time waiting for the app to become interactive.
Efficient Resource Usage: Prevents unnecessary code from being loaded, saving bandwidth and processing power.
Improved Caching: Shared modules are cached and reused across different parts of the application, reducing the number of server requests.
Key Concepts for Code Splitting
To understand how code splitting works, let’s first explore two key concepts: dynamic imports and lazy loading. These concepts work together to make code splitting possible and effective.
What are Dynamic Imports?
Dynamic imports are a JavaScript feature that allows you to load modules (or chunks of code) asynchronously at runtime, rather than loading everything upfront. Instead of using a regular import
statement (which loads all the code immediately), you use the import()
function, which returns a Promise. This means the code is only loaded when it’s actually needed.
Example: Static Import vs. Dynamic Import
// Static import (loads immediately)
import myModule from './myModule.js';
// Dynamic import (loads only when needed)
import('./myModule.js').then((module) => {
module.default(); // Use the module after it's loaded
});
In the example above:
The static import loads
myModule.js
as soon as the app starts.The dynamic import loads
myModule.js
only when theimport()
function is called.
This means the code inside myModule.js
won’t be downloaded until it’s actually required, which is the foundation of code splitting.
What is Lazy Loading?
Lazy loading is a design pattern that delays the loading of non-essential resources (like JavaScript modules, components, or images) until they are actually needed. This helps improve performance by reducing the amount of code loaded upfront.
How Does Lazy Loading Work?
Lazy loading of javascript modules is typically implemented using dynamic imports (
import()
). For example, instead of loading a component immediately, you load it only when the user interacts with a specific feature or navigates to a specific route.Also, different frameworks provide built-in ways to implement lazy loading. For instance React uses
React.lazy
and Vue usesdefineAsyncComponent
to lazy-load components.
Tools for Code Splitting
Code Splitting is implemented using a combination of framework-specific tools and general-purpose build tools. These tools work hand in hand to achieve efficient code splitting:
Framework-Specific Tools: These are built into frameworks like React, Vue, and Angular. They provide easy-to-use features to define split points and lazy-load components.
General-Purpose Build Tools: They handle the actual splitting of code into smaller chunks and optimise the bundles for production.
Framework Specific Tools
Tool | Framework | Key Feature |
React.lazy | React | Built-in React feature for lazy-loading components with Suspense . |
Next.js | React | Automatic route-based splitting and dynamic imports. |
defineAsyncComponent | Vue | Vue’s feature for lazy-loading components using dynamic imports. |
Angular Router | Angular | Router-based lazy loading with loadChildren . |
Vue Router | Vue.js | Lazy-loaded routes using dynamic imports. |
General Purpose Build Tools
Tool | Key Feature |
Webpack | Built-in support for dynamic imports and shared dependency optimization. |
Vite | Native ESM support for fast builds and automatic code splitting. |
Rollup | Supports dynamic imports and manual chunking. |
Parcel | Zero-configuration code splitting with dynamic imports. |
Snowpack | Native ESM support for fast builds and code splitting. |
Rsbuild | High-performance Rust-powered build system with efficient code splitting. |
Practical Implementation of Code Splitting
Now, let’s explore how code splitting works under the hood using a simple React app with the following files:
- App.js (Main entry point)
import React from 'react';
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom';
import HomePage from './HomePage';
import Dashboard from './Dashboard';
const App = () => {
return (
<Router>
<nav>
<Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
);
};
export default App;
- HomePage.js
import React from 'react';
const HomePage = () => {
return <div>Welcome to the Home Page!</div>;
};
export default HomePage;
- Dashboard.js
import React from 'react';
const Dashboard = () => {
return <div>Welcome to the Dashboard!</div>;
};
export default Dashboard;
In this version:
Initial Load: The browser downloads the entire JavaScript bundle (e.g.,
main.js
), which includesApp.js
,HomePage.js
, andDashboard.js
.No On-Demand Loading: Since all components are bundled together, the app loads everything upfront, which can lead to slower initial load times especially for larger applications.
Adding Code Splitting to the App
Step 1: Specifying Split Points
The developer defines split points in the code using the framework-specific tool.
Let’s modify App.js
to use React.lazy
and Suspense
for lazy loading:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom';
// Lazy-load the components
const HomePage = lazy(() => import('./HomePage'));
const Dashboard = lazy(() => import('./Dashboard'));
const App = () => {
return (
<Router>
<nav>
<Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
};
export default App;
What’s Happening Here?
We have specified that
HomePage
andDashboard
should be loaded on demand using lazy loading.<Suspense fallback={<div>Loading...</div>}>
provides a fallback UI while the components are being loaded.
Step 2: Build Tool Analyses the Code
Once the developer specifies the split points, the build tool (e.g., Webpack, Vite) analyses the application’s codebase to:
Identify dependencies and entry points.
Create a dependency graph, which maps out how all the modules (files) in the application are connected.
For the example above, the dependency graph might look like this:
App.js
├── HomePage.js
└── Dashboard.js
This graph tells the tool that:
App.js
depends onHomePage.js
andDashboard.js
.HomePage.js
andDashboard.js
are independent of each other.
Step 3: Build Tool Splits the Code
Based on the dependency graph and split points, the build tool divides the code into smaller chunks. The splitting is done based on:
Dynamic Imports: Code that is loaded only when needed (e.g.,
import('./HomePage')
).Entry Points: Different parts of the application (e.g., homepage, dashboard) can be split into separate bundles.
Shared Dependencies: Common modules (e.g., libraries like
React
) are split into shared bundles to avoid duplication.
Step 4: Build Tool Generates Bundles
The build tool generates multiple smaller bundles instead of one large bundle. Here’s what the output might look like after code splitting:
dist/
├── main.js
├── HomePage.chunk.js
└── Dashboard.chunk.js
main.js (Initial bundle containing
App.js
and shared dependencies likeReact
).HomePage.chunk.js (Chunk for the
HomePage
component).Dashboard.chunk.js (Chunk for the
Dashboard
component).
Step 5: Loading Bundles in the Browser
When the application runs in the browser, the code splitting technique comes into play:
Initial Load:
The browser loads
main.js
, which contains the core application logic and theSuspense
fallback UI.The code for
HomePage
andDashboard
is not included in the initial bundle.
On-Demand Loading:
When the user navigates to the home page, the browser dynamically fetches
HomePage.chunk.js
and renders theHomePage
component.When the user navigates to the dashboard, the browser dynamically fetches
Dashboard.chunk.js
and renders theDashboard
component.
Note: Caching and Reusing Bundles
Shared Dependencies: If multiple chunks depend on the same module (e.g.,
React
), the build tool ensures that the module is only included once in a shared bundle. This reduces duplication and improves caching.Browser Caching: Once a chunk is loaded, it is cached by the browser. If the user revisits the same part of the application, the chunk is loaded from the cache instead of being downloaded again.
Conclusion
Code splitting is a powerful technique to optimise web applications by loading only the necessary code upfront and fetching additional chunks on demand. By combining dynamic imports, lazy loading, and the right tools (both framework-specific and general-purpose build tools), developers can significantly improve performance, reduce load times, and enhance user experience.
Subscribe to my newsletter
Read articles from MyCodingNotebook directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

MyCodingNotebook
MyCodingNotebook
MyCodingNotebook is every developer’s online notebook—a space to explore coding concepts, share insights, and learn best practices. Whether you're just starting or an experienced developer, this blog serves as a go-to resource for coding best practices, real-world solutions, and continuous learning. Covering topics from foundational programming principles to advanced software engineering techniques, MyCodingNotebook helps you navigate your coding journey with clarity and confidence. Stay inspired, keep learning, and continue growing in your coding journey.