← Back to React Course | Chapter 13: Advanced React & Architecture | Lesson 5 of 14

Code Splitting with React.lazy

Code splitting with React.lazy is like only packing the suitcase for the country you're visiting right now, instead of carrying luggage for every country you might ever go to.

Splitting Code with React.lazy

React.lazy() takes a function that calls the dynamic import() syntax, returning a Promise for that component's module. The bundler (Vite, webpack) automatically splits that component into its own separate file, only downloaded when it's actually needed at runtime.

Note: This is most valuable for large components or entire routes not needed on the very first page load, like a settings page or an admin panel.

Warning: React.lazy's imported function must resolve to an object with a default export — a named export alone won't work directly with React.lazy.

Example: Splitting Code with React.lazy

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
const LazySettings = React.lazy(() =>
  Promise.resolve({ default: () => <p>Settings page (loaded on demand)</p> })
);
function App() {
  const [show, setShow] = React.useState(false);
  return (
    <div>
      <button onClick={() => setShow(true)}>Load Settings</button>
      {show && <React.Suspense fallback={<p>Loading settings...</p>}><LazySettings /></React.Suspense>}
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Lazy Loading Routes

One of the most common and impactful uses of React.lazy is per-route code splitting: each page of your app becomes its own chunk, so a user visiting only the homepage never downloads the code for the admin dashboard or settings page they never visit.

Note: Combine this with React Router's Route element prop — each lazy-loaded page component still just plugs in normally.

Warning: Every lazy-loaded route needs to be wrapped in Suspense somewhere in the tree (often once, around the whole Routes block) — forgetting it breaks navigation to that route.

Example: Lazy Loading Routes

markup
// Run in your local React project (npm install required)
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

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

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

⚠️ This example uses an npm package with no CDN build available here — run this in your local React project.

Weighing the Loading Flash Tradeoff

Lazy loading trades a smaller initial bundle for a brief loading state the first time a particular chunk is needed. For components that are ALWAYS needed right away (like the main navigation), this tradeoff isn't worth it — the loading flash adds friction with no real bundle-size benefit for that specific piece.

Note: Reserve lazy loading for things NOT needed immediately on first render — always-visible layout pieces should stay in the main bundle.

Warning: Lazy-loading something that's always immediately visible just adds an unnecessary loading flicker with no actual performance win.

Example: Weighing the Loading Flash Tradeoff

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
// Good candidate: rarely visited, large feature
const AdminPanel = React.lazy(() => Promise.resolve({ default: () => <p>Admin</p> }));

// Poor candidate: always visible immediately -- keep this in the main bundle
function MainNav() { return <nav>Always here, no lazy loading needed</nav>; }
function App() { return <MainNav />; }
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Forgetting React.lazy must always be paired with a Suspense boundary wrapping it, or it throws an error.
  2. Lazy-loading components that are always needed immediately (like the main layout), adding an unnecessary loading flash for no benefit.
  3. The dynamic import() function must return a module with a default export — React.lazy doesn't support named exports directly.
Chapter Summary
  • React.lazy(() => import('./Component')) splits a component into its own separate JS chunk, loaded only when needed.
  • This reduces the initial bundle size, improving the app's first-load performance.
  • A lazy component must always be rendered inside a Suspense boundary with a fallback.
  • Good candidates for lazy loading: routes, modals, and rarely-used features not needed on first render.
Browser Support

React.lazy available since React 16.6; relies on the bundler (Vite/webpack) supporting dynamic import().

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.