代码分割与懒加载
TanStack Router 将路由配置分为关键配置(路径解析、loader、搜索参数验证)和非关键配置(组件、错误/加载组件),支持多种方式的代码分割,可将初始包体积减少约 50%。
路由配置拆分
路由配置 = 关键部分(必须同步加载) + 非关键部分(可懒加载)
关键部分:path, params.parse, validateSearch, loader, beforeLoad, staticData
非关键部分:component, errorComponent, pendingComponent, notFoundComponent🔬 深入原理:Loader 不会被分割——因为 loader 本身已经是异步的,再包装一层
React.lazy会增加额外的延迟。组件才是代码分割的主要目标。
方式一:自动代码分割(推荐)
在 Vite 配置中启用,自动拆分所有非关键路由配置:
// vite.config.ts
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true, // ✨ 一行开启,零配置
}),
react(), // 必须在 tanstackRouter 之后
],
})💡 最佳实践:新项目直接开启
autoCodeSplitting: true。它兼容所有基于文件的路由,无需修改任何路由文件。
方式二:.lazy.tsx 后缀
将一个路由拆分为两个文件:
主文件(关键配置):
// src/routes/posts/$postId.tsx
// 只保留 loader、beforeLoad 等关键配置
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
return fetchPost(params.postId)
},
staleTime: 30_000,
})懒加载文件(非关键配置):
// src/routes/posts/$postId.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
import PostDetail from '@/components/PostDetail'
export const Route = createLazyFileRoute('/posts/$postId')({
component: PostDetail,
errorComponent: PostError,
pendingComponent: PostSkeleton,
})💡 最佳实践:如果主文件只包含 loader 且很简单,可以考虑删除主文件——Vite 插件会自动生成虚拟路由(仅保留基本的路径解析)。
方式三:lazyRouteComponent(代码路由)
用于代码路由模式的懒加载:
import { lazyRouteComponent } from '@tanstack/react-router'
// 默认导出
const route = createRoute({
path: '/posts/$postId',
component: lazyRouteComponent(() => import('./PostDetail')),
})
// 命名导出
const route = createRoute({
path: '/posts/$postId',
component: lazyRouteComponent(
() => import('./PostDetail'),
'PostDetailComponent', // 导出的名称
),
})lazyRouteComponent 返回一个 React.lazy 包装的组件,并附带 .preload() 方法用于预加载。
方式四:Loader 懒加载(谨慎使用)
Loader 本身异步,分割会引入双重异步延迟。仅在 loader 本身代码量特别大时考虑:
import { lazyFn } from '@tanstack/react-router'
const route = createRoute({
path: '/heavy-route',
component: HeavyComponent,
loader: lazyFn(() => import('./heavyLoader'), 'heavyLoader'),
})🚨 陷阱:分割 loader 意味着:发起请求 → 下载 loader 代码 → 执行 loader → 等待数据。而不分割是:发起请求 → 等待数据。额外的网络往返可能抵消 bundle size 的收益。仅当 loader 代码超过 10KB+ 时才考虑。
方式五:手动 React.lazy + Suspense
你也可以用手动方式懒加载组件:
import { Suspense, lazy } from 'react'
import { createFileRoute } from '@tanstack/react-router'
const HeavyComponent = lazy(() => import('@/components/HeavyComponent'))
export const Route = createFileRoute('/heavy-page')({
component: () => (
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
),
})💡 最佳实践:优先使用前三种方式(
autoCodeSplitting、.lazy.tsx、lazyRouteComponent),它们是 TanStack Router 原生支持的,与路由的pendingComponent和预加载机制集成得更好。
代码分割效果对比
| 策略 | 配置复杂度 | Bundle 体积优化 | 预加载支持 |
|---|---|---|---|
| 不分割 | 零 | 无 | N/A |
autoCodeSplitting |
一行配置 | ~50% 初始体积减少 | 自动 |
.lazy.tsx |
需要创建额外文件 | ~50% 初始体积减少 | 手动触发 |
lazyRouteComponent |
需要修改路由定义 | ~50% 初始体积减少 | 手动触发 |
React.lazy 手动 |
需要 Suspense 包裹 | ~50% 初始体积减少 | 无原生支持 |
预加载与代码分割的配合
代码分割和预加载策略可以组合使用:
// 全局配置 intent 预加载
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 鼠标悬停时预加载
defaultPreloadDelay: 100, // 100ms 延迟
})
// 关键路径使用 render 策略(立即预加载)
<Link to="/dashboard" preload="render">
仪表盘
</Link>
// 重型页面禁用预加载
<Link to="/analytics" preload={false}>
数据分析
</Link>交互流程:
用户悬停在 "仪表盘" 链接上
↓ 100ms 后
下载 dashboard.lazy.tsx 对应的 JS chunk
↓ (同时)
如果启用了 intent 预加载,同样触发 loader 预加载
↓
用户点击链接
↓
组件和数据都已在缓存中 → 即时渲染 ✅⚡ 性能提示:
intent+autoCodeSplitting是最佳默认组合。用户几乎感觉不到懒加载的延迟,因为数据下载在鼠标悬停时已经开始了。
检查分割效果
使用 Vite 的构建分析工具:
npm install -D rollup-plugin-visualizer// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
tanstackRouter({ autoCodeSplitting: true }),
react(),
visualizer({ open: true }), // 构建后打开依赖分析
],
})💡 最佳实践:定期检查构建产物。如果某个 chunk 特别大(>100KB),考虑进一步拆分其内部组件。