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 the import() 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 uses defineAsyncComponent 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

ToolFrameworkKey Feature
React.lazyReactBuilt-in React feature for lazy-loading components with Suspense.
Next.jsReactAutomatic route-based splitting and dynamic imports.
defineAsyncComponentVueVue’s feature for lazy-loading components using dynamic imports.
Angular RouterAngularRouter-based lazy loading with loadChildren.
Vue RouterVue.jsLazy-loaded routes using dynamic imports.

General Purpose Build Tools

ToolKey Feature
WebpackBuilt-in support for dynamic imports and shared dependency optimization.
ViteNative ESM support for fast builds and automatic code splitting.
RollupSupports dynamic imports and manual chunking.
ParcelZero-configuration code splitting with dynamic imports.
SnowpackNative ESM support for fast builds and code splitting.
RsbuildHigh-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 includes App.js, HomePage.js, and Dashboard.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 and Dashboard 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 on HomePage.js and Dashboard.js.

  • HomePage.js and Dashboard.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 like React).

  • 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:

  1. Initial Load:

    • The browser loads main.js, which contains the core application logic and the Suspense fallback UI.

    • The code for HomePage and Dashboard is not included in the initial bundle.

  2. On-Demand Loading:

    • When the user navigates to the home page, the browser dynamically fetches HomePage.chunk.js and renders the HomePage component.

    • When the user navigates to the dashboard, the browser dynamically fetches Dashboard.chunk.js and renders the Dashboard 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.

10
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.