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
: WhyReact.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: Thefallback
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 multipleSuspense
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:
import React, { Suspense } from 'react';
: We importSuspense
from React.const HomePage = React.lazy(() => import('./pages/HomePage'));
:- Instead of a direct static import, we assign the result of
React.lazy()
toHomePage
. - The function passed to
React.lazy
callsimport('./pages/HomePage')
. This tells the bundler (like Webpack) to create a separate chunk forHomePage.jsx
and its unique dependencies. - The same is done for
AboutPage
andContactPage
.
- Instead of a direct static import, we assign the result of
<Suspense fallback={...}>
:- We wrap the
<Routes>
component (or any part of the tree containing lazy components) with<Suspense>
. - The
fallback
prop is given a simplediv
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.
- We wrap the
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
, orContactPage.jsx
. - When a user navigates to
/about
, React will attempt to renderAboutPage
. Since it's lazy-loaded,Suspense
will show the "Loading page..." fallback while theAboutPage.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.
-
Run your React application (e.g.,
npm start
oryarn start
). -
Open your browser's Developer Tools (usually by pressing F12 or right-clicking and selecting "Inspect").
-
Navigate to the "Network" tab.
-
Filter by "JS" to see only JavaScript files.
-
Clear the network log (optional, for a cleaner view).
-
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 like1.<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
---------------------------------------------------- - You should see an initial set of JS files (chunks). These are your main bundle, vendor chunks (if any), and possibly a chunk for
-
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
---------------------------------------------------- - Observe the Network tab again. You should see a new JavaScript chunk being downloaded. This is the code for
-
Navigate to the "Contact" page.
- You'll see another new chunk for
ContactPage.jsx
being loaded.
- You'll see another new chunk for
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 dynamicimport()
.Suspense
is mandatory when usingReact.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.