Lazy Loading and Code Splitting in React
Lazy Loading in React
~10 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
To wrap up Phase 4: how to make sure the browser doesn't have to download the code for EVERY single page before the app even starts – only the code for the page the user is actually looking at right now.
The problem: an ever-growing bundle
Without special measures, Vite packs the ENTIRE JavaScript code of our app – ProductListPage, ProductDetailPage, ReviewsPage, LoginPage, AccountPage, and everything they import – into ONE single file (the "bundle"), which the browser must download in full on the very first page visit. For a small app like ours that's not a problem, but for real, large applications with dozens of pages, the bundle can grow to several megabytes, and the initial load time can become noticeably slow.
The solution: React.lazy() + Suspense
React.lazy() turns a regular import into a DEFERRED import – the code for that page only gets downloaded once it's actually needed ("code splitting"). Since downloading takes time, React needs something to display in the meantime – that's what <Suspense> is for.
Updating App.jsx: lazy-loading less-used pages
We selectively lazy-load pages that not every visitor needs (login, account, reviews) – the home page ProductListPage and the product detail page stay as regular imports, since they're the most-used pages:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { useAuth } from './context/AuthContext';
import { useDocumentTitle } from './hooks/useDocumentTitle';
import ProductListPage from './pages/ProductListPage';
import ProductDetailPage from './pages/ProductDetailPage';
import ProtectedRoute from './components/ProtectedRoute';
const ReviewsPage = lazy(() => import('./pages/ReviewsPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
const AccountPage = lazy(() => import('./pages/AccountPage'));
function App() {
const { user, login, logout } = useAuth();
useDocumentTitle('Product Catalog');
return (
<div className="app">
<header className="app-header">
<h1>Product Catalog</h1>
{user ? (
<p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
) : (
<button onClick={() => login('Jane Doe')}>Log in</button>
)}
</header>
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<ProductListPage />} />
<Route path="/products/:sku" element={<ProductDetailPage />}>
<Route path="reviews" element={<ReviewsPage />} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route
path="/account"
element={
<ProtectedRoute>
<AccountPage />
</ProtectedRoute>
}
/>
</Routes>
</Suspense>
</div>
);
}
export default App;Two changes work together: const ReviewsPage = lazy(() => import('./pages/ReviewsPage')) instead of a regular import ReviewsPage from './pages/ReviewsPage' at the top of the file, AND <Suspense fallback={{...}}> around <Routes>, which shows what should be visible while loading.
| Vanilla JS/HTML | React |
|---|---|
All <script> tags get downloaded immediately when the page loads | lazy() pages only get downloaded once that route is actually visited |
| No built-in concept for a "loading state while downloading more" | <Suspense fallback={{...}}> automatically shows a loading state |
Open the "Network" tab in your browser's dev tools, load the home page, then navigate to /login: you'll see a NEW, separate JavaScript file being downloaded, exactly at that moment – before that, its code simply wasn't part of the downloaded bundle. That wraps up Phase 4 (Routing).
Tipp: Rule of thumb: lazy-load pages that (a) are rarely visited (login, account, settings) or (b) bring in especially large dependencies (e.g. a charting or PDF library). Keeping the home page and other frequently visited core pages as regular imports usually remains the better choice, since the extra loading step itself also costs time.