main bundle vs. lazy chunk — what ships where
A route is split into two files. The non-lazy file
(chart.tsx) holds the route config — path,
validateSearch, beforeLoad, and the generated types.
It is bundled into the main bundle so every <Link to="/chart"> and
useSearch() stays type-safe without pulling in the heavy chart code.
The lazy file (chart.lazy.tsx) holds the component and
loader — it becomes its own chunk, fetched on demand.
| piece | bundle | why |
|---|---|---|
createFileRoute config(path, validateSearch, beforeLoad) |
main bundle (~always) |
needed for type-safe <Link>, useSearch, and matching before the route mounts |
createLazyFileRoute(component + loader) |
lazy chunk (~on demand) |
heavy runtime code (charts, editors, maps) — only paid for when the route is visited |
| route types (inferred) | main bundle | type info is erased at compile time; it never reaches the wire |
<Suspense> boundary |
main bundle | router auto-wraps the lazy route; shows a fallback while the chunk loads |
1 · the code-split router you write (edit me)
2 · Babel compiles JSX → element tree
<CodeSplitDemo/> becomes React.createElement(CodeSplitDemo).
lazyChart.useComponent() returns the cached component or null (the
<Suspense> fallback handles the gap). The status poll re-renders the bundle bar
as the chunk transitions idle → loading → loaded.
// (hit "compile & render" to see Babel's output)
3 · live React (the split, proven)
Click Chart to navigate to the lazy route — watch the "Loading chunk…" state, then the chunk resolves (~600ms) and the bar grows to ~250KB. Hit prefetch first and the next navigation is instant (cache hit). The gold-check runs all of this automatically.
chunk: — · gold: home → chart (loading → loaded, ~250KB) → home
intent → pattern
| intent | pattern | why |
|---|---|---|
| split a heavy route | createLazyFileRoute('/x')({{component, loader}}) in x.lazy.tsx |
component + loader ship in their own chunk; fetched only when visited |
| keep links type-safe | leave createFileRoute('/x')({{validateSearch, beforeLoad}}) in x.tsx (main bundle) |
types + matching config are always present; <Link to="/x"> validates |
| avoid the loading flash | <Link preload="intent"> or router.preloadRoute() |
fetch the chunk on hover/viewport; by click time it's cached → instant |
| show a fallback while loading | wrap in <Suspense fallback={{…}}> (router does this per-route) |
lazy component returns a promise; Suspense renders the fallback |
| split ONLY a component (not a route) | use React.lazy() + <Suspense> instead |
createLazy is route-scoped and also splits the loader — React.lazy cannot |