Every byte of JS your user downloads is a byte they paid for. The bytes they never visit aren't worth paying for. Code splitting is how you ship only what each page needs.
The mechanism
const Heavy = React.lazy(() => import('./Heavy')). The dynamic import() tells Vite/Rollup 'this is a separate chunk.' At build time, Heavy ends up in its own JS file. At runtime, the chunk loads only when <Heavy /> is rendered. Combine with <Suspense> so the loading state is declarative.
Three places to split
- Per route — every page is a separate chunk. The user pays for the home page; clicking a deep link loads only that page's bundle.
- Per heavy component — a charting library, a Markdown renderer, anything >50KB used in only one or two places.
- Per modal / dialog — a settings modal might pull in form libraries; lazy-load when the user opens it.
The cost
Each chunk is a network round trip. Splitting too aggressively means waterfalls of small requests, which can be slower than one big bundle. Aim for chunks 30-200KB gzipped; smaller than that doesn't justify the round-trip cost.
Preloading
If you know the user is about to navigate to a route, you can preload its chunk. <link rel="modulepreload" href="..."> in HTML, or programmatically by simply touching the lazy component. Vite emits modulepreload tags for entry chunks automatically.