Skip to content
代码分割与懒加载

代码分割与懒加载

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.tsxlazyRouteComponent),它们是 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),考虑进一步拆分其内部组件。