Skip to content

React Tutorial Advanced React Concepts

Lazy Loading Components in React with React.lazy

Lesson 120 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

A user who opens your home page does not need the code for the admin screen, the chart library or the rich-text editor. Lazy loading (code splitting) downloads that code only when it is needed, so the first load is smaller and faster.

The mechanism

  • Dynamic import() loads a JavaScript module on demand and returns a promise. Bundlers such as Vite turn each import() into a separate file (a chunk).
  • React.lazy(() => import("./Heavy.jsx")) wraps that promise as a component.
  • <Suspense fallback={...}> shows a placeholder while the chunk downloads.
App.jsx
JSX
import { lazy, Suspense } from 'react'

const Reports = lazy(() => import('./Reports.jsx'))   // not downloaded yet

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

The module must export default the component (for named exports, wrap with .then((m) => ({ default: m.Reports }))).

Try it: simulated chunk loading

The playground has no separate files, so we simulate a slow chunk with a promise. Everything else, lazy, Suspense and the fallback, is the real thing:

App.jsx
JSX
function HeavyChart() {
  return <p>Chart loaded (pretend this came from a big library)</p>;
}

// Stand-in for: lazy(() => import("./HeavyChart.jsx")), with a delay to mimic the network
const LazyChart = React.lazy(
  () => new Promise((resolve) => setTimeout(() => resolve({ default: HeavyChart }), 900))
);

function App() {
  const [show, setShow] = React.useState(false);

  return (
    <div>
      <h3>Dashboard</h3>
      <button onClick={() => setShow(true)}>Show chart</button>
      {show && (
        <React.Suspense fallback={<p>Loading chart...</p>}>
          <LazyChart />
        </React.Suspense>
      )}
    </div>
  );
}

Nothing about the chart is loaded until you click. The fallback appears while lazy‘s promise is pending, then the component replaces it. (A lazy component’s promise is cached, so showing it a second time is instant.)

Where to split

Good candidates are large pieces that most visits do not use:

Split byExample
Route (the most common)Each page is a lazy import
Rarely used featureAn editor, a chart, a PDF viewer, a map
Modals and tabs not yet openedSettings dialogs
Heavy librariesImport them inside the lazy component

Do not split tiny components. Every chunk costs a network request, and over-splitting hurts.

A verified build

This small project was built with Vite, and the build was checked: it produced more than one JavaScript file, and the lazy component ended up in its own chunk.

src/HeavyPanel.jsx
JSX
function HeavyPanel() {
  return <p>The heavy panel has loaded</p>
}

export default HeavyPanel
src/App.jsx
JSX
import { lazy, Suspense, useState } from 'react'

const HeavyPanel = lazy(() => import('./HeavyPanel.jsx'))

function App() {
  const [show, setShow] = useState(false)

  return (
    <div>
      <h1>Lazy loading demo</h1>
      <button onClick={() => setShow(true)}>Load the panel</button>
      {show && (
        <Suspense fallback={<p>Loading...</p>}>
          <HeavyPanel />
        </Suspense>
      )}
    </div>
  )
}

export default App

You can see it yourself: run npm run build and look at the dist/assets folder, or open the browser’s Network tab and click the button.

Lazy routes

The best place for lazy loading is the router. In data mode, a route can use lazy: () => import("./pages/Reports.jsx") (Route Loaders and Lazy Routes). With declarative routes, wrap the lazy pages in one <Suspense>:

App.jsx
JSX
const Reports = lazy(() => import('./pages/Reports.jsx'))

<Suspense fallback={<p>Loading...</p>}>
  <Routes>
    <Route path="/reports" element={<Reports />} />
  </Routes>
</Suspense>

Preloading

To avoid a delay on click, start loading before the user asks: on hover, on focus or when the browser is idle. Call the import function early (import("./Reports.jsx")); the module is then already cached when lazy needs it.

Handle failure

A chunk can fail to load (offline, or you deployed a new version and the old filename is gone). Wrap lazy components in an error boundary (Error Boundaries) with a “Reload” button.

Key points

  • lazy(() => import("./X.jsx")) plus <Suspense fallback> downloads code only when needed.
  • Split by route and by large, rarely used features; do not split tiny components.
  • Add an error boundary for failed chunks, and preload when you can predict the click.