Performance
Optimize bundle sizes, code splitting, and loading performance in VitNode and TanStack Start.
TanStack Start splits components into separate lazy chunks by default, keeping the initial entry bundle light. Follow these key practices to maintain instant page loads.
Performance Checklist
- Keep
headmetadata isolated: Never import UI components into routehead. - Lazy-load heavy dialogs: Use
React.lazy+Suspensefor complex editors and dialogs. - Dynamic imports inside loaders: Lazy-load heavy server/client utilities with
await import(...). - Optimize images: Use
loading="lazy", responsive widths, and explicit dimensions. - Analyze your bundle: Regularly check client chunk sizes.
What Lands in the Client Entry Bundle
TanStack Router automatically extracts route component, errorComponent, and notFoundComponent into lazy chunks:
| Route Option | Read When | In Initial Bundle? | Optimization |
|---|---|---|---|
path, id | Route tree generation | Yes | Keep route paths concise |
head | Page navigation | Yes | Isolate metadata strings in leaf files |
loader | Pre-render | Yes (fn only) | await import() large dependencies inside fn |
pendingComponent | Route loading | Yes | Use lightweight SVG/CSS skeleton primitives |
component | Render | No (Lazy Chunk) | Automatically code-split |
Best Practices
1. Isolate Metadata from Components
Because head is evaluated in the main route bundle, importing strings from component files accidentally pulls entire component trees into the initial download:
// BAD: Pulls HomeRouteContent and all its heavy icons/charts into main entry
import { HOME_TITLE, HomeRouteContent } from "#/site/home/home-content"
// GOOD: Metadata strings live in a lightweight leaf file
import { HomeRouteContent } from "#/site/home/home-content"
import { HOME_DESCRIPTION, HOME_TITLE } from "#/site/home/metadata"
export const Route = createFileRoute("/_main/")({
head: () =>
pageHead({
title: HOME_TITLE,
description: HOME_DESCRIPTION,
}),
component: HomeRouteContent,
})2. Lazy Dialogs and Heavy Form Editors
Heavy editors (like Tiptap) or complex modals should be lazy-loaded with React.lazy and Suspense:
import React, { Suspense } from "react"
import { Loader } from "@vitnode/core/components/ui/loader"
const RichEditor = React.lazy(async () =>
import("@vitnode/core/components/form/fields/editor").then((mod) => ({
default: mod.AutoFormEditor,
}))
)
export const ArticleEditor = (props) => (
<Suspense fallback={<Loader />}>
<RichEditor {...props} />
</Suspense>
)3. Dynamic Imports Inside Loaders
When a route loader requires a heavy calculation or parsing library, import it dynamically:
export const Route = createFileRoute("/_main/stats")({
loader: async () => {
// Only downloaded when visitor navigates to /stats
const { calculateStats } = await import("#/features/stats/calculator")
return calculateStats()
},
})4. Lazy Images Below the Fold
Always specify explicit aspect ratios and loading="lazy" on images outside the initial viewport:
<img
src="/photo.webp"
alt="Community event"
width={800}
height={600}
loading="lazy"
decoding="async"
className="aspect-4/3 object-cover rounded-lg"
/>Measuring Bundle Size
Analyze your client bundle with Vite:
bun run buildpnpm buildnpm run buildInspect the output chunk sizes in apps/web/dist/. Keep any single lazy chunk below 150 KB for optimal mobile performance.