Code Splitting & Lazy Loading
Lazy route component
Split each route's component into its own chunk with a dynamic import(). Pass component as an async factory:
import { createRoute } from "@domphy/router"
export const DashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/dashboard",
component: async () => {
const { Dashboard } = await import("./pages/Dashboard.js")
return Dashboard
},
})The router resolves the component before rendering — users see the pending UI while the chunk loads.
Pending UI during load
Show a loading state while a lazy component fetches:
export const DashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/dashboard",
component: async () => {
const { Dashboard } = await import("./pages/Dashboard.js")
return Dashboard
},
pendingComponent: () => ({ div: "Loading dashboard…" }),
pendingMinMs: 200, // don't flash the pending UI for fast loads
})pendingMinMs (default 0) sets a minimum display time — prevents a flash when the chunk is already cached.
Preloading on hover
Preload a route's chunk before the user clicks — reduces perceived latency:
import { createRoute, preloadRoute } from "@domphy/router"
const NavLink = (to: string, label: string) => ({
a: label,
href: to,
onMouseenter: () => preloadRoute({ to }), // fires on hover
onClick: (e) => { e.preventDefault(); router.navigate({ to }) },
})preloadRoute resolves the component and loader concurrently. Subsequent navigation uses the cached result instantly.
Lazy loader
Loaders can also be lazy. Combine with the component for a single chunk:
export const SettingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/settings",
loader: async () => {
const { loadSettings } = await import("./loaders/settings.js")
return loadSettings()
},
component: async () => {
const { SettingsPage } = await import("./pages/Settings.js")
return SettingsPage
},
})Chunk grouping with Vite
Group related routes into a single chunk to reduce round-trips:
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
"admin": [
"./src/pages/Admin.ts",
"./src/pages/Users.ts",
"./src/pages/Roles.ts",
],
},
},
},
},
}Then import from the same chunk:
const AdminRoute = createRoute({
path: "/admin",
component: async () => {
const { Admin } = await import("./pages/Admin.js") // triggers "admin" chunk load
return Admin
},
})Route-level error handling
Catch chunk load failures (network errors, cache invalidation):
const LazyRoute = createRoute({
path: "/reports",
component: async () => {
try {
const { Reports } = await import("./pages/Reports.js")
return Reports
} catch {
const { ErrorPage } = await import("./pages/ErrorPage.js")
return () => ErrorPage({ message: "Failed to load this page." })
}
},
errorComponent: ({ error }) => ({
div: `Error: ${error.message}`,
}),
})Preload intent
Use router.preloadRoute() with intent: "hover" | "render" to control when routes start loading:
// Preload on idle after initial render — good for "next likely page"
requestIdleCallback(() => {
router.preloadRoute({ to: "/dashboard" })
})Critical path vs async chunks
Keep the initial bundle lean by marking non-critical imports as lazy:
// BEFORE — entire charting library in initial bundle
import { Chart } from "chart.js"
// AFTER — loaded only when the analytics page is actually visited
const AnalyticsRoute = createRoute({
path: "/analytics",
component: async () => {
await import("chart.js") // side-effect import
const { Analytics } = await import("./Analytics.js")
return Analytics
},
})Bundle analysis
After vite build, inspect chunk sizes:
vite build --mode analyze
npx vite-bundle-analyzer dist/stats.htmlAim for:
- Initial bundle (router + core + theme): < 30 kB gzip
- Per-route chunks: < 10 kB gzip for simple pages, < 50 kB for heavy ones