react-code-splitting-lazy-suspense

Code Splitting in React with Lazy and Suspense

  • 4 min

In the previous chapter we optimized the execution speed of our components. Today we will optimize the initial load speed.

By default, when we build a React application (using Vite, Webpack or Create React App), the build tool takes all our hundreds of JavaScript files and puts them in a blender to create a single gigantic file (usually called index.js or bundle.js).

This has a serious problem: The user has to download the ENTIRE application just to see the first screen.

If your app has a very heavy “Admin Panel” that only 1% of users use, why force the remaining 99% to download that panel’s code?

The solution is Code Splitting. The idea is to chop that giant file into small pieces and load them only when the user needs them.

React.lazy: Dynamic Import

In standard JavaScript, we are used to static imports at the top of the file:

// ❌ Static Import: Always downloaded, whether used or not.
import PanelAdmin from './pages/PanelAdmin';
Copied!

React offers us a function called lazy that allows us to define a component that will be imported dynamically.

import { lazy } from 'react';

// ✅ Dynamic Import: Only downloaded when React tries to render it.
const PanelAdmin = lazy(() => import('./pages/PanelAdmin'));
Copied!

Notice the import() syntax. It is a function that returns a Promise. Vite will automatically detect this and, instead of putting PanelAdmin in the main bundle, it will create a separate file (e.g., PanelAdmin-XyZ.js) that will stay on the server waiting to be called.

Suspense: Managing the Wait

Since downloading the PanelAdmin-XyZ.js file over the network takes some time (milliseconds or seconds), React asks itself:

What on earth do I show on the screen while the component arrives?

If you try to render a lazy component without anything else, React will throw an error and the app will crash.

We must wrap the lazy component inside a parent component called Suspense.

Suspense accepts a mandatory prop called fallback, which is what React will render while waiting.

import { lazy, Suspense } from 'react';

// 1. Define the lazy component
const HeavyWidget = lazy(() => import('./components/HeavyWidget'));

export default function App() {
  return (
    <div>
      <h1>My Dashboard</h1>
      
      {/* 2. Wrap with Suspense and define the loading state */}
      <Suspense fallback={<div className="spinner">Loading widget...</div>}>
        <HeavyWidget />
      </Suspense>
    </div>
  );
}
Copied!

Route-based Splitting Pattern

Although you can do lazy loading of a button or a modal, the place where this technique usually makes the most sense is in the Router.

We want each page (/home, /about, /contact) to be a separate “chunk”. This way, the initial load of the website is instant because we only download the Home page.

Let’s refactor our App.jsx:

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

// 1. Transform static imports into lazy ones
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard')); // Suppose this one is 2MB

function App() {
  return (
    <div>
      <Navbar />
      
      {/* 2. Wrap ALL routes in a single Suspense */}
      <Suspense fallback={<h3>Loading page... ⏳</h3>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </div>
  );
}
Copied!

Now, if you open the browser’s “Network” tab:

  1. When entering /, only the main JS and the Home code are downloaded.
  2. When clicking “Dashboard”, you’ll see a new network request downloading the Dashboard code, the fallback will appear for an instant, and then the page will show.

UX: Avoiding the Loading Flash

Code Splitting is great for performance, but it can be bad for User Experience (UX) if we overuse it.

If you put lazy on very small components or ones that load very quickly, the user will see a constant flickering of spinners (Flash of Loading Content).

Practical Tips

  1. Split by Routes: This is the safest and most effective approach.
  2. Split VERY Heavy Components: Interactive maps, rich text editors, chart libraries (Chart.js / D3), or complex modals that users may never open.
  3. Don’t Split “Above the Fold” Components: Do not lazily load the Header or main Banner, because the user will see a broken web page for the first milliseconds.