React Code Splitting: lazy, Suspense and Faster First Load

React.js Full Course: Beginner to Advanced · Advanced React · 7 min read · Updated

Load code only when needed

  1. The first visit downloads only the code of the home page, so it loads fast.
  2. Later the person opens the admin page.
  3. lazy starts a dynamic import and downloads the admin code only now.
  4. Suspense shows a fallback while the file loads, then shows the page.
  5. Split by route first, then heavy libraries. Preload on hover to hide the wait.

When you build a React app, all your code is joined into files called bundles. The browser must download and run them before the page works. If your app is large, the first visit can be slow, especially on a phone with a weak connection. Code splitting means loading only the code a visitor needs now, and loading the rest later.

The idea in plain words

Think of a restaurant. You do not bring the whole kitchen to the table when someone sits down. You bring the menu first, and the food when it is ordered. In a website, the home page is the menu. The admin dashboard, the chart library and the settings page are dishes that only some visitors will order, so load them when asked.

Dynamic import

JavaScript has a feature called dynamic `import()`. Unlike the normal import at the top of a file, it loads a file when you call it and returns a promise. The build tool (Vite) sees this and puts that file in a separate bundle.

button.addEventListener('click', async () => {
  const { exportToPdf } = await import('./exportToPdf.js');
  exportToPdf(data);
});

The PDF code is only downloaded when someone clicks the button. Visitors who never export never pay for it.

React.lazy and Suspense

React gives you lazy to use this idea for components, and Suspense to show something while the code is loading.

import { lazy, Suspense } from 'react';

const Reports = lazy(() => import('./Reports.jsx'));

function App() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading reports...</p>}>
        <Reports />
      </Suspense>
    </div>
  );
}
  • lazy(() => import('./Reports.jsx')) makes a component that loads on first use. The file must export default the component.
  • Suspense shows its fallback until the lazy component is ready. Give a helpful fallback, such as a spinner or a skeleton that has the same size as the real content, so the page does not jump.
  • You can put several lazy components inside one Suspense, and it waits for all.

The best place to split: routes

The most useful and simple place is at the page level. Each route becomes its own bundle, so a visitor to the home page does not download the code of the admin pages.

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

const Home = lazy(() => import('./pages/Home.jsx'));
const Products = lazy(() => import('./pages/Products.jsx'));
const Admin = lazy(() => import('./pages/Admin.jsx'));

function App() {
  return (
    <Suspense fallback={<p>Loading page...</p>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/admin" element={<Admin />} />
      </Routes>
    </Suspense>
  );
}

One Suspense around all the routes is enough to start. You can add smaller ones later around slow parts, so the rest of the page stays visible.

Split heavy libraries too

Big libraries, such as chart tools, rich text editors, maps and PDF makers, are often the largest part of a bundle. Load them only where they are used:

const Chart = lazy(() => import('./Chart.jsx'));   // Chart.jsx imports the big chart library

function Stats({ show }) {
  return show ? (
    <Suspense fallback={<div style={{ height: 300 }}>Loading chart...</div>}>
      <Chart />
    </Suspense>
  ) : null;
}

Preload: load before it is needed

A lazy page loads when the person goes there, which can feel like a small delay. You can load it a little earlier, for example when the person moves the mouse over the link:

const loadProducts = () => import('./pages/Products.jsx');
const Products = lazy(loadProducts);

<Link to="/products" onMouseEnter={loadProducts} onFocus={loadProducts}>Products</Link>

The browser keeps the loaded file, so when the page is requested it is ready at once. This keeps the benefit of splitting without the wait.

What happens when loading fails?

A lazy file can fail to load, for example if the person goes offline, or if you deployed a new version and the old file name no longer exists. React then throws an error. You should catch it with an error boundary, which the next lesson covers, and show a message such as “Could not load this page. Please reload.”

Check your bundle size

Look at what npm run build prints. Vite shows each file and its size, and warns when a file is larger than 500 kilobytes. Many teams also use a visual tool, such as rollup-plugin-visualizer, which draws a map of what is in your bundle, so you can spot a single huge package.

  • Prefer smaller libraries, or import only the part you need (for example one function, not the whole utility library).
  • Remove packages you no longer use.
  • Compress images, and use modern formats.
  • Make sure your server sends files compressed (gzip or brotli) and with good cache headers. Files with a hash in their name can be cached for a long time.

Measure the result

Open the browser tools on the Network tab, turn on throttling (for example “Fast 4G”), reload the page, and look at how many kilobytes were downloaded before the page shows. Compare before and after splitting. Also test with a real phone if you can. Tools like Lighthouse in Chrome give a score and tips.

Common mistakes

  • Forgetting export default in a file used with lazy.
  • No Suspense around a lazy component, which gives an error.
  • A fallback that is much smaller than the real content, so the page jumps when it loads.
  • Making tiny components lazy, which adds many small requests for no gain.
  • Not handling load failures.

A real example of the numbers

Here is the kind of difference splitting can make. Imagine an app with a home page, a dashboard with a chart library, and an admin area. Before splitting, the build produces one file of about 900 kilobytes. Every visitor downloads all of it, even those who never open the dashboard. After making the dashboard and admin pages lazy, the main file might drop to about 250 kilobytes, and the chart code is a separate file of 500 kilobytes that only loads for people who open the dashboard. A visitor to the home page now downloads about a quarter of what they did before.

These numbers are only an example. Your own results depend on your libraries and pages. The point is the method: build, look at the file sizes, split the biggest parts that are not needed on the first screen, and build again to compare. A few minutes of work can make the first visit much faster, especially on a phone with a slow connection.

Your task

  1. Take your router app from lesson 18 and make each page lazy.
  2. Run npm run build before and after. Compare the size of the main file.
  3. Add a preload on hover for one link.
  4. In the browser Network tab, check that a page's file loads only when you open that page.

How to check it worked

  • The first load downloads less code than before.
  • Each page's own file appears in the Network tab only when you visit it.
  • The fallback appears briefly, then the page.

Summary

  • Code splitting loads code only when it is needed, so the first visit is faster.
  • Use lazy(() => import('./Page.jsx')) and wrap the result in Suspense with a fallback.
  • Split at routes first, then heavy libraries. Preload on hover to avoid delays.
  • Check bundle size and always handle load failures.

Frequently asked questions

What is code splitting?

Dividing your app code into several files and loading each one only when the visitor needs it, so the first page loads faster.

Do I need Suspense with lazy?

Yes. Suspense shows a fallback while the lazy component loads. Without it React throws an error.

Where should I split my code?

Start with routes (one bundle per page), then split heavy libraries such as charts and editors.

Why does the page jump when a lazy component loads?

The fallback is smaller than the real content. Make the fallback about the same size, for example a skeleton.