Routes are the natural split points: a visitor on the dashboard needs the dashboard's code and nothing else, and the router knows which module each URL wants. A React Router 8 139,386 route object takes lazy either as a function returning a whole route definition or — usually better — as an object whose keys load independently, so a data loader can fetch while the component chunk is still in flight.
import { createBrowserRouter, RouterProvider, Outlet } from 'react-router';
import Home from './Home.jsx';
const Shell = () => <><nav>...</nav><Outlet /></>;
const load = (p) => async () => (await p()).default;
const router = createBrowserRouter([{ path: '/', Component: Shell, children: [
{ index: true, Component: Home },
{ path: 'reports', lazy: { Component: load(() => import('./Reports.jsx')) } },
{ path: 'customers', lazy: { Component: load(() => import('./Customers.jsx')) } },
] }]);
export default () => <RouterProvider router={router} />;dist/assets/Customers-B1uhhsX8.js 8.81 kB │ gzip: 3.35 kB dist/assets/index-D4Tnu0Gh.js 312.59 kB │ gzip: 98.65 kB dist/assets/Reports-C0KU1_ZQ.js 336.44 kB │ gzip: 98.15 kB
The entry chunk is 312.59 kB, about 55 kB more than the same split done with <BrowserRouter> and lazy components, because the data router carries loaders, actions and fetchers — a cost you buy back the moment a route pulls in a charting library. path, id, index, caseSensitive and children may not come from a lazy object: the router needs them to match the URL first. Split at the route, not at every component — fifteen 4 kB chunks load more slowly over a high-latency link than one 60 kB chunk. In data mode the router keeps the current page on screen while the next chunk downloads, so there is no fallback flash; read useNavigation().state to show progress instead.