Code Splitting and Lazy Loading in React

As a React application grows, the amount of JavaScript code bundled into the final build increases. By default, the entire application is bundled into a single large file that the browser must download before anything appears on screen. For large applications, this can make the initial page load noticeably slow.

Code splitting solves this by breaking the application into smaller bundles that are loaded on demand. Lazy loading is the technique of loading parts of the application only when they are needed — not all at once. Together, these techniques drastically improve the initial load performance of large React applications.

React.lazy — Loading Components on Demand

React provides React.lazy() to dynamically import a component only when it is actually rendered. Instead of loading the entire application upfront, only the components needed for the current view are loaded.


import { lazy } from 'react';

// Instead of: import Dashboard from './pages/Dashboard';
const Dashboard = lazy(() => import('./pages/Dashboard'));

The import is wrapped in a function that returns a promise. React waits for the component to load before rendering it.

Suspense — Showing a Fallback While Loading

Since a lazy-loaded component takes time to download, something needs to be shown while the user waits. The Suspense component handles this by displaying a fallback UI during the loading period:


import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<p>Loading, please wait...</p>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

The fallback prop accepts any JSX — a loading spinner, a skeleton screen, or just a simple message. React shows the fallback while the lazy component's bundle is being downloaded, then swaps it out once the component is ready.

Combining Lazy Loading with React Router

The most impactful use of lazy loading is with routes. Each page of the application can be lazily loaded — only the current page's code is downloaded on navigation. This means the user only downloads the code they actually visit:


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

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link> |
        <Link to="/about">About</Link> |
        <Link to="/dashboard">Dashboard</Link>
      </nav>

      <Suspense fallback={<div>Loading page...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

A user who never visits the Dashboard page will never download the Dashboard component's code. This significantly reduces the initial bundle size.

How Code Splitting Works Behind the Scenes

When using Vite or Webpack (the tools that bundle React projects), a dynamic import() call tells the bundler to split that module into a separate chunk file. These chunk files are downloaded by the browser only when they are needed. The bundler handles all of this automatically — no manual configuration is needed when using React.lazy().

Creating a Better Loading Experience

The fallback in Suspense can be any component — including a custom loading spinner:


function LoadingSpinner() {
  return (
    <div>
      <p>⏳ Loading content...</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        {/* Routes here */}
      </Routes>
    </Suspense>
  );
}

Multiple Suspense Boundaries

Multiple Suspense components can be used at different levels of the tree. This allows fine-grained control over which parts of the UI show a loading state:


function Dashboard() {
  return (
    <div>
      <h2>Dashboard</h2>

      <Suspense fallback={<p>Loading chart...</p>}>
        <LazyChart />
      </Suspense>

      <Suspense fallback={<p>Loading table...</p>}>
        <LazyDataTable />
      </Suspense>
    </div>
  );
}

The chart and table each have their own loading state. If the chart loads before the table, it appears immediately without waiting for the table.

Limitations of React.lazy

  • React.lazy() only supports default exports. Named exports must be re-exported as default in a wrapper module.
  • It only works in client-side rendered applications. For server-side rendering, additional setup is required.

Key Points

  • Code splitting breaks a large JavaScript bundle into smaller pieces loaded on demand.
  • React.lazy() enables dynamic imports — components are downloaded only when they are rendered.
  • Suspense provides a fallback UI displayed while a lazy component loads.
  • Route-based lazy loading is the most impactful optimization strategy for large applications.
  • The bundler (Vite or Webpack) automatically creates separate chunk files for lazily imported modules.
  • Multiple Suspense boundaries allow independent loading states for different parts of the UI.

The next topic covers Error Boundaries — a mechanism for gracefully handling JavaScript errors in the component tree without crashing the entire application.

Leave a Comment

Your email address will not be published. Required fields are marked *