Code splitting
By default, a bundler rolls your whole app into one JavaScript file, and every import at the top of a file adds to it. That single bundle grows with the app, and the browser has to download all of it before anything renders.
On a fast machine with strong Wi-Fi you may never notice. On a phone with a weak connection, one heavy library can block the entire first load, including features that need nothing from that library. The course demonstrates this with a product list that depends on a large fake-data package: with everything in one bundle, even the counter on the same page stays blank for over twenty seconds on a throttled connection.
Code splitting is the fix. It breaks the bundle into pieces, called chunks, and downloads a chunk only if and when the user actually needs it. Features the user never touches are never downloaded at all.
Dynamic import()
The primitive underneath is plain JavaScript. A top-of-file import always runs. Calling import() as a function instead loads a module on demand, from wherever in your code you call it, and returns a promise that resolves with the module.
// always loaded, part of the main bundle
import { formatPrice } from './format'
// loaded only when this handler runs
async function showChart() {
const { renderChart } = await import('./charts')
renderChart()
}Bundlers treat every import() call as a cut line: the imported module, plus the dependencies it does not share with the main bundle, get their own chunk, fetched over the network the first time the call runs.
lazy and Suspense
React wraps this primitive in a pair of tools. lazy takes a function that returns a dynamic import() and gives you back a component you can render like any other. The loader's promise has to resolve to a module whose default export is the component; other named exports alongside it are fine. For a file you do not control that only exports named components, adapt the promise instead: lazy(() => import('./charts').then(m => ({ default: m.RevenueChart }))).
Rendering a lazy component suspends: React pauses rendering that part of the tree until the chunk arrives. On its own that is an error, so you wrap the lazy component in a Suspense boundary with a fallback prop, some UI to show while the download is in flight.
import { lazy, Suspense, useState } from 'react'
const ProductsList = lazy(() => import('./ProductsList'))
export default function App() {
const [showProducts, setShowProducts] = useState(false)
return (
<>
<Counter />
<button onClick={() => setShowProducts(prev => !prev)}>
Show products
</button>
{showProducts && (
<Suspense fallback={<h2>Loading...</h2>}>
<ProductsList />
</Suspense>
)}
</>
)
}Now the main bundle no longer contains ProductsList or its heavy dependency. The counter appears immediately. Clicking the button starts the chunk download, the fallback shows while it loads, and the component renders when it lands. After that first load the module is cached, so hiding and showing it again is instant.
Version note
lazy and Suspense are named imports from react. Older code, including the course lessons, writes them as React.lazy and React.Suspense; they are the same APIs.
Split on routes first
You could hunt for individual heavy components, but the default cut line in most apps is the route. Each page is a natural unit: a visitor on the home page has no use for the dashboard's code, so make every page a lazy component and let routing decide which chunks ever download.
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const Vans = lazy(() => import('./pages/Vans'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<h2>Loading...</h2>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/vans" element={<Vans />} />
<Route path="/host" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}One Suspense boundary around the routes covers every page. First load fetches the main bundle plus only the chunk for the current URL; other pages download when the user navigates to them. That breadth has a cost: navigating to a page whose chunk has not arrived swaps the entire route area for the fallback, so keep shared layout like the header and nav outside the boundary and they stay put across the swap.
Splitting has a floor, though. Every chunk costs an extra network request and a moment of fallback UI, so splitting a tiny component saves nothing worth that price. Leave small components, and anything above the fold that users see immediately, in the main bundle. Split what is heavy, rarely used, or behind a navigation.
You wrap a dynamic import in lazy, put the component inside Suspense with a fallback like a loading message, and React shows the fallback while the code downloads. The rest of your app appears right away.
Next up: Authentication, where sessions, tokens, and server-side enforcement come together.

