Performance

Route-level code splitting: load only the code a page needs

Route-level code splitting splits the bundle into one file per page, so a first visit downloads only what it needs. How to set it up and what to measure.

Key points

  • Route-level code splitting moves code the page never uses out of the first load.
  • The build of 1 November 2026 emitted 48 JavaScript files totalling 523,037 bytes, 176,211 gzipped.
  • The entry stays at 141,302 bytes: the gain comes from the chunks you skip.

Route-level code splitting splits the bundle into one file per page, so a first visit downloads only the code it needs. The build of this site emits 48 JavaScript files, yet the home page asks for one.

One dynamic import per route

import { lazy, Suspense } from "react";

const Components = lazy(() => import("./pages/Components"));
const Blocks = lazy(() => import("./pages/Blocks"));

export function Routes({ tab }) {
  return (
    Loading}>
      {tab === "components" ?  : }
    
  );
}

Each import() creates a split point: Vite emits one file for a module reached only from that point. Code shared by several routes lands in the common chunk, not duplicated in each one.

Four ways to split

TechniqueWhat it separatesWhen it helps
import() per routeone page per filea SPA with several views
manualChunksshared dependencies in one fileheavy libraries used everywhere
prefetch on idlethe next route chunkpredictable navigation
conditional importcode behind a flageditors and admin panels

Where the gain shows

The entry does not shrink: it stays at 141,302 bytes because React and the router live there. The gain sits in the chunks the home page never requests: Blocks weighs 22,130 bytes and arrives only when you open that page. On a single home visit the saving is the sum of the chunks you skip.

The command

npx vite build --mode production

The output lists every file with its size in kB and writes client/dist. Add --sourcemap when you need to see which module ends up in which chunk.

The limitation

Build sizes are not transferred bytes: 48 files exist on disk, and one visit downloads a few of them. Splitting too finely creates extra round trips on deep routes, and a duplicated chunk costs more than the entry you tried to avoid. Read the requests in the Network panel, not the dist folder.

Where to continue

The EneUI catalogue indexes 111,985 components and 1,556 blocks with previews. The record sidebar is a component made of several files: import it statically and it lands in the entry. For the weight of one import read an import that grows the bundle, for compression Brotli versus gzip, for the measured comparison the weight of UI libraries, and for the CSS that blocks the paint critical CSS.

Frequently asked questions

What is the difference between code splitting and tree shaking?

Tree shaking removes code that is never imported, while code splitting divides what remains into files loaded on demand. You need both: the first cuts the total, the second cuts what starts at once.

Should every route get its own file?

No. Split the heavy routes and the ones visited rarely; a 3 KB route gains nothing from an extra file. Group small views and keep one chunk for the large ones.

Does the split chunk arrive before or after the first paint?

After, because the browser requests it when the component mounts. With React.lazy the request starts on the first render of the route, so the Suspense fallback is what the visitor sees meanwhile.

How do I stop a library from landing in several chunks?

Declare a manual group in build.rollupOptions.output.manualChunks for shared dependencies. Rollup extracts the module into a common chunk and every route imports it from there.

Can code splitting make loading metrics worse?

It can hurt LCP when the main content sits inside a chunk loaded late. Keep whatever paints the first screen in the entry and split the rest.