LearnContact
Lesson 4320 min read

Lazy Loading

Learn how React.lazy() and <Suspense> let you split your app into separate bundle chunks that download only when a route or component is actually needed.

Introduction

By default, a bundler like Vite or webpack packages your entire application — every page, every component — into one big JavaScript file that the browser must download before anything can render. As an app grows, that single bundle grows too, slowing down the very first page load even for users who will only ever visit one page.

Lazy loading fixes this by splitting the app into smaller chunks and downloading each chunk only when it is actually needed. This lesson covers React.lazy(), the <Suspense> component that shows a loading state while a chunk downloads, and how to combine both with React Router so each page becomes its own separate bundle.

What You Will Learn
  • What lazy loading means and why it improves initial load time.
  • How to use React.lazy() to dynamically import a component.
  • How <Suspense> shows a fallback UI while a lazy component downloads.
  • A worked example of lazy loading a single component.
  • How to lazy load React Router routes so each page is a separate chunk.

What is Lazy Loading?

Lazy loading means delaying the download of a piece of code until the moment it is actually needed, instead of bundling everything upfront. For example, the code for a rarely-visited "Settings" page does not need to be downloaded by every visitor the instant they land on the home page — it can wait until they actually navigate there.

Smaller Initial Bundle

Users download only the code needed for the first screen they see.

Faster First Load

Less JavaScript to parse and execute means the app becomes interactive sooner.

Code Splitting

The bundler automatically creates separate chunk files for each lazy-loaded piece.

Loaded On Demand

Each chunk downloads the first time it is actually rendered, then stays cached.

React.lazy()

React.lazy() takes a function that calls a dynamic import() and returns a special component. React does not download that component's code until the very first time it is rendered — the dynamic import() is what tells the bundler to split that file into its own chunk.

Basic React.lazy() Syntax
import { lazy } from 'react';

const Settings = lazy(() => import('./pages/Settings'));
Just a Regular Import, Wrapped

import('./pages/Settings') is a dynamic import that returns a Promise. React.lazy() wraps that Promise so it behaves like a normal component once the code has finished downloading.

The <Suspense> Component

While a lazy component's chunk is downloading, React needs something to show on screen. <Suspense> wraps around lazy components and displays its "fallback" prop — usually a spinner or skeleton — until the download finishes, then swaps in the real component.

Wrapping with Suspense
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Settings />
    </Suspense>
  );
}

export default App;

Example: Lazy Loading a Component

Imagine a heavy "Chart" component that uses a large charting library. Most users never open the tab that shows it, so lazy loading keeps that big dependency out of the initial bundle entirely.

Dashboard.jsx
import { lazy, Suspense, useState } from 'react';

const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>

      {showChart && (
        <Suspense fallback={<p>Loading chart...</p>}>
          <Chart />
        </Suspense>
      )}
    </div>
  );
}

export default Dashboard;
What Happens on Click
Button clicked
  -> setShowChart(true)
  -> Chart chunk begins downloading
  -> "Loading chart..." shown briefly
  -> Chart chunk finishes downloading and renders

Lazy Loading Routes

The most common and effective place to use lazy loading is at the route level. Each page in your app becomes its own chunk, so visiting the home page never downloads the code for the settings page, the profile page, or any other route the user has not visited yet.

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

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

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<p>Loading page...</p>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

export default App;

Why Wrap <Routes> in One <Suspense>

  • A single <Suspense> around all routes shows one fallback while whichever matched page is downloading.
  • Each route component still becomes its own separate chunk in the final build.
  • You can also nest smaller, more local <Suspense> boundaries for individual heavy components within a page.

Common Mistakes

Avoid These Mistakes
  • Forgetting to wrap a lazy component in <Suspense> — React will throw an error if there is no Suspense boundary above it.
  • Lazy loading tiny components that barely affect bundle size, adding unnecessary complexity for little benefit.
  • Using a named import with React.lazy() — the dynamically imported module must have a default export.
  • Assuming lazy loading fixes slow code — it only delays when code downloads, not how fast that code runs.

Best Practices

  • Lazy load at the route level first — it gives the biggest, easiest win for initial load time.
  • Reserve component-level lazy loading for genuinely heavy, rarely-used pieces like charts or rich text editors.
  • Keep fallback UIs simple and fast, like a small spinner, so the loading state itself feels lightweight.
  • Make sure every lazily imported module has a default export, since React.lazy() expects one.
  • Measure bundle size before and after splitting to confirm the change actually helps.

Frequently Asked Questions

Does lazy loading make my code run faster?

No. It does not speed up execution — it only delays downloading code until it is needed, which reduces the initial bundle size and speeds up the first load.

What happens if I forget the <Suspense> boundary?

React will throw an error, because a lazy component needs a Suspense ancestor to know what to render while its code is still downloading.

Can I use React.lazy() with named exports?

Not directly. React.lazy() expects the dynamically imported module to have a default export; for a named export, you can re-export it as default in a small wrapper file.

Does every route need its own Suspense boundary?

No, one shared Suspense wrapping all your routes is common and simple. You can add extra nested boundaries later for finer-grained loading states.

Is lazy loading only useful for routes?

No. Any large, conditionally-rendered component — a modal, a chart, a rich editor — is a good lazy loading candidate, not just top-level pages.

Key Takeaways

  • Lazy loading delays downloading a component's code until it is actually rendered.
  • React.lazy() takes a dynamic import() and returns a component React can render once its chunk has loaded.
  • <Suspense> displays a fallback UI while a lazy component's code is still downloading.
  • Lazy loading route components is one of the most effective ways to shrink an app's initial bundle size.
  • Each lazily imported module must have a default export.

Summary

Lazy loading lets your app grow without forcing every user to download all of it upfront. React.lazy() and <Suspense> work together to split code into chunks and show a graceful loading state while those chunks download on demand.

In this lesson, you learned what lazy loading solves, how React.lazy() and <Suspense> work together, and how to apply this pattern to React Router routes. Next, you will learn how error boundaries prevent a single broken component from crashing your entire app.

Lesson 43 Completed
  • You understand what lazy loading means and why it helps initial load time.
  • You can use React.lazy() to dynamically import a component.
  • You can wrap lazy components in <Suspense> with a fallback UI.
  • You can lazy load React Router routes so each page is its own chunk.
Next Lesson →

Error Boundaries