Route-Based Code Splitting

Introduction

As React applications grow in size, the initial bundle can become large, leading to slower page loads and decreased user experience. One of the most effective ways to improve performance is route-based code splitting, which loads only the code necessary for the current route.

React provides built-in tools like React.lazy and Suspense that make lazy loading components straightforward.

In this article, we will cover:

  1. Benefits of code splitting for performance.
  2. How to use React.lazy and Suspense with React Router routes.
  3. Practical examples of lazy loading page components.
  4. Combining lazy loading with nested routes for large applications.

Benefits of Route-Based Code Splitting

  1. Faster Initial Load
    • Only the code needed for the initial route is downloaded.
  2. Reduced Bundle Size
    • Large components and rarely used pages are split into separate chunks.
  3. Improved User Experience
    • Faster page loads and responsive navigation.
  4. Better Performance on Mobile Networks
    • Lazy loading reduces the data transferred, improving load times for slow connections.
  5. Scalable Architecture
    • Applications remain maintainable as code grows, with clear separation between route chunks.

Using React.lazy for Lazy Loading

React.lazy allows components to be loaded dynamically only when they are rendered.

Syntax

const SomeComponent = React.lazy(() => import('./SomeComponent'));
  • Returns a lazy-loaded component.
  • Must be rendered inside a Suspense component that provides a fallback while loading.

Using Suspense with Lazy-Loaded Components

Suspense allows you to show a loading indicator while a lazy-loaded component is being fetched.

Example

import React, { Suspense } from "react";

const Home = React.lazy(() => import("./Home"));

function App() {
  return (
<Suspense fallback={<div>Loading...</div>}>
  <Home />
</Suspense>
); } export default App;
  • fallback can be any React element, such as a spinner or placeholder text.
  • Improves user experience while waiting for the component to load.

Lazy Loading with React Router

Lazy loading works seamlessly with React Router to split code by route.

Example: Basic Route-Based Lazy Loading

import React, { Suspense } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

const Home = React.lazy(() => import("./Home"));
const About = React.lazy(() => import("./About"));
const Contact = React.lazy(() => import("./Contact"));

function App() {
  return (
<Router>
  <Suspense fallback={<div>Loading...</div>}>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  </Suspense>
</Router>
); } export default App;

Explanation

  • Each page component (Home, About, Contact) is loaded only when its route is visited.
  • The initial bundle is smaller, improving performance.

Example: Lazy Loading Page Components

Step 1: Create Page Components

// Home.js
export default function Home() {
  return <h1>Home Page</h1>;
}

// About.js
export default function About() {
  return <h1>About Page</h1>;
}

// Contact.js
export default function Contact() {
  return <h1>Contact Page</h1>;
}

Step 2: Lazy Load in Routes

import React, { Suspense } from "react";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";

const Home = React.lazy(() => import("./Home"));
const About = React.lazy(() => import("./About"));
const Contact = React.lazy(() => import("./Contact"));

function App() {
  return (
&lt;Router&gt;
  &lt;nav&gt;
    &lt;Link to="/"&gt;Home&lt;/Link&gt; | 
    &lt;Link to="/about"&gt;About&lt;/Link&gt; | 
    &lt;Link to="/contact"&gt;Contact&lt;/Link&gt;
  &lt;/nav&gt;
  &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
    &lt;Routes&gt;
      &lt;Route path="/" element={&lt;Home /&gt;} /&gt;
      &lt;Route path="/about" element={&lt;About /&gt;} /&gt;
      &lt;Route path="/contact" element={&lt;Contact /&gt;} /&gt;
    &lt;/Routes&gt;
  &lt;/Suspense&gt;
&lt;/Router&gt;
); } export default App;

Explanation

  • Users see the loading fallback while the component code is being fetched.
  • Components not needed for the current route are not loaded initially, improving speed.

Combining Lazy Loading with Nested Routes

For larger apps, nested routes can also be lazy-loaded to optimize performance further.

Example: Nested Routes

import React, { Suspense } from "react";
import { BrowserRouter as Router, Routes, Route, Outlet, Link } from "react-router-dom";

const Dashboard = React.lazy(() => import("./Dashboard"));
const Profile = React.lazy(() => import("./Profile"));
const Settings = React.lazy(() => import("./Settings"));

function DashboardLayout() {
  return (
&lt;div&gt;
  &lt;h1&gt;Dashboard&lt;/h1&gt;
  &lt;nav&gt;
    &lt;Link to="profile"&gt;Profile&lt;/Link&gt; | 
    &lt;Link to="settings"&gt;Settings&lt;/Link&gt;
  &lt;/nav&gt;
  &lt;Suspense fallback={&lt;div&gt;Loading Section...&lt;/div&gt;}&gt;
    &lt;Outlet /&gt;
  &lt;/Suspense&gt;
&lt;/div&gt;
); } function App() { return (
&lt;Router&gt;
  &lt;Suspense fallback={&lt;div&gt;Loading Page...&lt;/div&gt;}&gt;
    &lt;Routes&gt;
      &lt;Route path="/" element={&lt;div&gt;Home Page&lt;/div&gt;} /&gt;
      &lt;Route path="/dashboard" element={&lt;DashboardLayout /&gt;}&gt;
        &lt;Route index element={&lt;Dashboard /&gt;} /&gt;
        &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
        &lt;Route path="settings" element={&lt;Settings /&gt;} /&gt;
      &lt;/Route&gt;
    &lt;/Routes&gt;
  &lt;/Suspense&gt;
&lt;/Router&gt;
); } export default App;

Explanation

  • DashboardLayout contains a nested Outlet for child routes.
  • Each child (Profile, Settings) is lazy-loaded only when visited.
  • Nested lazy loading further reduces initial bundle size.

Benefits of Nested Lazy Loading

  1. Fine-Grained Performance Control
    • Only the necessary chunks are loaded, even within a parent route.
  2. Faster Navigation Between Sections
    • Lazy-loaded sections reduce the amount of code executed per route.
  3. Better Scalability
    • Large applications with many pages or dashboard sections benefit the most.

Handling Loading States Gracefully

Example: Custom Loading Spinner

function Loading() {
  return (
&lt;div style={{ textAlign: "center", marginTop: "50px" }}&gt;
  &lt;p&gt;Loading...&lt;/p&gt;
  &lt;div className="spinner"&gt;&lt;/div&gt;
&lt;/div&gt;
); } <Suspense fallback={<Loading />}> <Routes> ... </Routes> </Suspense>
  • Users get a better visual experience than a plain “Loading…” text.
  • Can be combined with CSS animations for smooth effects.

Tips and Best Practices

  1. Split by Route, Not Component
    • Focus on pages or sections, not every small component.
  2. Use Meaningful Fallbacks
    • Show loading indicators or skeleton UI to improve UX.
  3. Combine with Error Boundaries
    • Catch lazy loading failures and display a fallback UI.
<Suspense fallback={<Loading />}>
  <ErrorBoundary>
&lt;Routes&gt; ... &lt;/Routes&gt;
</ErrorBoundary> </Suspense>
  1. Avoid Over-Lazy Loading Tiny Components
    • Lazy loading very small components can increase overhead.
  2. Prefetching Important Routes
    • Use dynamic import with webpackPrefetch to load critical pages in advance.
const About = React.lazy(() => import(/* webpackPrefetch: true */ './About'));

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *