How to Implement Code Splitting in React

This article provides a practical guide on how to implement code splitting in a React application to optimize performance and reduce initial bundle sizes. You will learn how to use dynamic imports, implement React.lazy and Suspense for component-level loading, and apply route-based splitting to ensure users only download the JavaScript code they actually need.

Understanding Dynamic Imports

By default, bundlers like Webpack pack all your React code into a single, large JavaScript file. Code splitting breaks this bundle into smaller chunks that can be loaded on demand. The foundation of code splitting in modern JavaScript is the dynamic import() syntax.

Instead of statically importing a module at the top of a file, you import it asynchronously inside your functions:

// Static Import (Bundled immediately)
import { add } from './math';

// Dynamic Import (Loaded on demand)
import('./math').then((math) => {
  console.log(math.add(16, 26));
});

When a build tool encounters this dynamic import syntax, it automatically starts code-splitting your application.

Using React.lazy and Suspense

For React components, the standard implementation of code splitting relies on React.lazy and the Suspense component. React.lazy lets you render a dynamically imported component just like a regular component.

Here is how to implement it:

import React, { lazy, Suspense } from 'react';

// Lazily load the component
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h1>My React Application</h1>
      {/* Suspense provides a fallback UI while the component loads */}
      <Suspense fallback={<div>Loading component...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

export default App;

The lazy function takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component. The Suspense component must wrap the lazy component to render a fallback UI (like a spinner or loading text) while the chunk is downloading.

Implementing Route-Based Code Splitting

One of the best places to introduce code splitting is at the route level. Web pages are naturally divided by URLs, so loading the code for a specific page only when the user navigates to it ensures a fast initial page load.

Below is an example of route-based code splitting using react-router-dom:

import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// Lazily load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading page...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

export default App;

In this setup, if a user lands on the Home page, they will not download the JavaScript required for the Dashboard or Settings pages until they actively navigate to those routes.

Handling Loading Errors

Network issues can cause dynamically loaded chunks to fail to load. To prevent your application from crashing, you should wrap your lazy components with an Error Boundary. This allows you to display a friendly error message and offer a retry option if a chunk fails to fetch.

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error("Chunk load failed:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h2>Something went wrong loading this section. Please refresh.</h2>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

You can then wrap your Suspense block with this boundary:

<ErrorBoundary>
  <Suspense fallback={<div>Loading...</div>}>
    <HeavyComponent />
  </Suspense>
</ErrorBoundary>