Skip to main content

Route-based Code Splitting with `React.lazy` (Part 1) #130

📖 Introduction

Following our exploration of The "Why" of Code Splitting, where we understood the critical performance benefits, this article delves into the practical "how" of implementing route-based code splitting in React using React.lazy. This function is React's built-in solution for dynamically loading components, making it straightforward to defer loading the code for different routes until they are actually needed.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • Understanding of code splitting benefits (covered in Article 129).
  • React Components and JSX.
  • React Router (react-router-dom) basics: <BrowserRouter>, <Routes>, <Route>, and <Link>.
  • JavaScript Promises and dynamic import() syntax (conceptual understanding).

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • What React.lazy Is: Understanding its purpose and how it works with dynamic imports.
  • Basic Implementation: Step-by-step guide to lazy-loading a component for a specific route.
  • The Role of Suspense: Why React.lazy must be used with a <Suspense> component and how to provide loading fallbacks.
  • Setting up a Basic Routing Example: Creating a simple multi-route application to demonstrate code splitting.
  • Verifying Code Splitting: Using browser developer tools to see the separate chunks being loaded.

🧠 Section 1: The Core Concepts of React.lazy and Suspense

1.1 - What is React.lazy?

React.lazy is a function provided by React that lets you render a dynamically imported component as a regular component. It makes code-splitting simple by allowing you to load components only when they are rendered.

How it works: React.lazy takes a function as an argument. This function must call a dynamic import(). The dynamic import() returns a Promise which resolves to a module with a default export containing the React component.

// Before React.lazy (standard import, part of main bundle)
// import AboutPage from './pages/AboutPage';

// With React.lazy (AboutPage code is split into a separate chunk)
const AboutPage = React.lazy(() => import('./pages/AboutPage'));

When React first tries to render AboutPage, it will trigger the dynamic import. While the AboutPage.js chunk is being fetched from the server, the rendering of AboutPage needs to be suspended. This is where React.Suspense comes in.

1.2 - What is React.Suspense?

React.Suspense is a component that lets you specify a loading indicator (a "fallback" UI) if some components in the tree below it are not yet ready to render (e.g., they are being lazy-loaded).

import React, { Suspense } from 'react';

const AboutPage = React.lazy(() => import('./pages/AboutPage'));
const HomePage = React.lazy(() => import('./pages/HomePage'));

function App() {
return (
<Suspense fallback={<div>Loading page content...</div>}>
{/* Assume routing logic here that decides which page to render */}
{/* <HomePage /> or <AboutPage /> */}
</Suspense>
);
}

Key Points about Suspense:

  • fallback Prop: The fallback prop accepts any React elements that you want to render while waiting for the lazy component to load. This could be a simple text message, a spinner, or a more sophisticated skeleton screen.
  • Placement: You can place Suspense components anywhere above a lazy component. You can even have multiple Suspense components at different levels of your application tree, each with its own fallback UI.
  • Not Just for Code Splitting: While React.lazy is a primary use case, Suspense is also designed for other asynchronous operations like data fetching (though this is still evolving in React).

Why are they used together? React.lazy initiates an asynchronous operation (loading the component code). React needs a way to handle the "in-between" state while the code is loading. Suspense provides this mechanism, allowing the rest of your application to remain interactive while displaying a user-friendly loading indicator for the part of the UI that is still pending. Without Suspense, React wouldn't know what to display while the lazy component is loading and would throw an error.


💻 Section 2: Implementing Basic Route-based Code Splitting

Let's build a small application with a few routes and see how to apply React.lazy and Suspense.

Project Setup (Conceptual): Assume we have a project structure like this:

src/
|-- components/
| |-- Navbar.jsx
|-- pages/
| |-- HomePage.jsx
| |-- AboutPage.jsx
| |-- ContactPage.jsx
|-- App.jsx
|-- index.js

HomePage.jsx (Example Page Component):

// src/pages/HomePage.jsx
import React from 'react';

function HomePage() {
return (
<div>
<h1>Welcome to the Home Page</h1>
<p>This is the main landing page of our application.</p>
</div>
);
}

export default HomePage;

AboutPage.jsx (Example Page Component):

// src/pages/AboutPage.jsx
import React from 'react';

function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company and team.</p>
</div>
);
}

export default AboutPage;

ContactPage.jsx (Example Page Component):

// src/pages/ContactPage.jsx
import React from 'react';

function ContactPage() {
return (
<div>
<h1>Contact Us</h1>
<p>Get in touch with us through this page.</p>
</div>
);
}

export default ContactPage;

2.1 - Standard (Non-Lazy) Routing Setup

First, let's look at a typical App.jsx with React Router without code splitting.

// src/App.jsx (Standard Imports)
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ContactPage from './pages/ContactPage';
import Navbar from './components/Navbar'; // Assume Navbar.jsx exists

function App() {
return (
<Router>
<Navbar />
<div className="container">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</div>
</Router>
);
}

export default App;

In this setup, HomePage, AboutPage, and ContactPage (and their dependencies) would all be included in the main JavaScript bundle loaded when the user first visits the site.

2.2 - Applying React.lazy and Suspense

Now, let's modify App.jsx to use React.lazy and Suspense.

// src/App.jsx (With React.lazy and Suspense)
import React, { Suspense } from 'react'; // Import Suspense
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

import Navbar from './components/Navbar';

// Lazy load the page components
const HomePage = React.lazy(() => import('./pages/HomePage'));
const AboutPage = React.lazy(() => import('./pages/AboutPage'));
const ContactPage = React.lazy(() => import('./pages/ContactPage'));

function App() {
return (
<Router>
<Navbar />
<div className="container" style={{ padding: '20px' }}>
{/* Wrap Routes with Suspense */}
<Suspense fallback={<div style={{ textAlign: 'center', marginTop: '50px', fontSize: '1.5em' }}>Loading page...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</Suspense>
</div>
</Router>
);
}

export default App;

Step-by-Step Code Breakdown:

  1. import React, { Suspense } from 'react';: We import Suspense from React.
  2. const HomePage = React.lazy(() => import('./pages/HomePage'));:
    • Instead of a direct static import, we assign the result of React.lazy() to HomePage.
    • The function passed to React.lazy calls import('./pages/HomePage'). This tells the bundler (like Webpack) to create a separate chunk for HomePage.jsx and its unique dependencies.
    • The same is done for AboutPage and ContactPage.
  3. <Suspense fallback={...}>:
    • We wrap the <Routes> component (or any part of the tree containing lazy components) with <Suspense>.
    • The fallback prop is given a simple div with a "Loading page..." message. This UI will be shown whenever React needs to load one of the lazy page components. You can style this fallback or use a custom loading spinner component.

Navbar.jsx (For completeness):

// src/components/Navbar.jsx
import React from 'react';
import { Link } from 'react-router-dom';

function Navbar() {
const navStyle = {
background: '#333',
padding: '10px',
marginBottom: '20px',
};
const linkStyle = {
color: 'white',
margin: '0 10px',
textDecoration: 'none',
};

return (
<nav style={navStyle}>
<Link to="/" style={linkStyle}>Home</Link>
<Link to="/about" style={linkStyle}>About</Link>
<Link to="/contact" style={linkStyle}>Contact</Link>
</nav>
);
}

export default Navbar;

index.js (Entry point):

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

Now, when the application loads:

  • The initial JavaScript bundle will contain App.jsx, Navbar.jsx, React, React Router, and other core dependencies.
  • It will not initially contain the code for HomePage.jsx, AboutPage.jsx, or ContactPage.jsx.
  • When a user navigates to /about, React will attempt to render AboutPage. Since it's lazy-loaded, Suspense will show the "Loading page..." fallback while the AboutPage.js chunk is downloaded and processed. Once loaded, AboutPage renders.

🛠️ Section 3: Verifying Code Splitting in Action

How do we know code splitting is actually working? We use the browser's developer tools.

  1. Run your React application (e.g., npm start or yarn start).

  2. Open your browser's Developer Tools (usually by pressing F12 or right-clicking and selecting "Inspect").

  3. Navigate to the "Network" tab.

  4. Filter by "JS" to see only JavaScript files.

  5. Clear the network log (optional, for a cleaner view).

  6. Load the application's root page (e.g., http://localhost:3000/).

    • You should see an initial set of JS files (chunks). These are your main bundle, vendor chunks (if any), and possibly a chunk for HomePage.jsx if it's the default route. The exact names and number of chunks depend on your bundler's configuration (e.g., Create React App, Vite, or custom Webpack).
    • For example, with Create React App, you might see files like main.<hash>.js, vendors~main.<hash>.js, and then possibly a numbered chunk like 1.<hash>.js for the home page if it's lazy-loaded.
    Example Initial Load (Network Tab):
    ----------------------------------------------------
    Name Status Type Size
    ----------------------------------------------------
    main.asdf123.js 200 script 150KB <-- Main app logic
    vendors~main.qwer456.js 200 script 500KB <-- React, ReactDOM, etc.
    1.zxcv789.js 200 script 10KB <-- HomePage chunk
    ----------------------------------------------------
  7. Navigate to another route (e.g., click the "About" link).

    • Observe the Network tab again. You should see a new JavaScript chunk being downloaded. This is the code for AboutPage.jsx (e.g., 2.poiu098.js).
    • While it's loading (you can throttle your network speed in DevTools to see this more clearly), the Suspense fallback UI ("Loading page...") will be displayed.
    After Navigating to /about (Network Tab):
    ----------------------------------------------------
    Name Status Type Size
    ----------------------------------------------------
    main.asdf123.js 200 script 150KB
    vendors~main.qwer456.js 200 script 500KB
    1.zxcv789.js 200 script 10KB
    2.poiu098.js 200 script 8KB <-- NEW! AboutPage chunk loaded
    ----------------------------------------------------
  8. Navigate to the "Contact" page.

    • You'll see another new chunk for ContactPage.jsx being loaded.

If you see these separate chunks being loaded on demand as you navigate, then route-based code splitting with React.lazy is working correctly! The browser is only fetching the code for each page when it's needed.


💡 Conclusion & Key Takeaways (Part 1)

In this first part, we've laid the groundwork for route-based code splitting in React. You've learned about React.lazy for defining components that should be loaded dynamically and React.Suspense for providing a fallback UI during the loading state. We've also walked through a basic implementation and how to verify its effects using browser developer tools.

Key Takeaways So Far:

  • React.lazy enables easy code splitting by taking a function that returns a dynamic import().
  • Suspense is mandatory when using React.lazy to define what to render while the lazy component's code is being fetched.
  • This combination allows different routes (pages) of your application to be split into separate JavaScript chunks, significantly improving initial load performance.

Challenge Yourself: Modify the Suspense fallback in the example application. Instead of a simple text message, try creating a more visually appealing loading spinner component and use that as the fallback. How does this change the user experience during navigation?


➡️ Next Steps

We've covered the basics of React.lazy and Suspense for route-based code splitting. In the next article, "Route-based Code Splitting with React.lazy (Part 2)", we will explore more advanced scenarios, including:

  • Handling errors when a lazy component fails to load.
  • Using React.lazy with named exports.
  • Considering where to place Suspense components for optimal user experience.
  • Preloading lazy components for even faster perceived navigation.

Stay tuned to further refine your React performance optimization skills!


glossary

  • React.lazy: A React function that lets you render a dynamically imported component as a regular component, enabling code splitting.
  • React.Suspense: A React component used to specify a loading indicator (fallback UI) if components in the tree below it are not yet ready to render (e.g., due to lazy loading).
  • Fallback UI: The UI (e.g., a loading spinner or message) displayed by Suspense while a lazy component is loading.
  • Route-based Code Splitting: The practice of splitting code based on application routes or pages, loading code for a specific page only when the user navigates to it.
  • Chunk: A smaller piece of JavaScript code that is split from the main bundle and can be loaded on demand.

Further Reading