code-splitting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Code Splitting

代码分割

TanStack Router separates route code into critical (required to match and start loading) and non-critical (can be lazy-loaded). The bundler plugin can split automatically, or you can split manually with
.lazy.tsx
files.
CRITICAL: Never
export
component functions from route files — exported functions are included in the main bundle and bypass code splitting entirely.
CRITICAL: Use
getRouteApi('/path')
in code-split files, NOT
import { Route } from './route'
. Importing Route defeats code splitting.
TanStack Router 将路由代码分为关键代码(匹配路由和启动加载所必需)和非关键代码(可懒加载)。打包器插件可以自动完成分割,你也可以通过
.lazy.tsx
文件手动分割。
重要警告:切勿从路由文件中
export
组件函数——导出的函数会被纳入主包,完全绕过代码分割机制。
重要警告:在代码分割文件中请使用
getRouteApi('/path')
,而非
import { Route } from './route'
。导入 Route 会使代码分割失效。

What Stays in the Main Bundle (Critical)

主包中保留的内容(关键代码)

  • Path parsing/serialization
  • validateSearch
  • loader
    ,
    beforeLoad
  • Route context, static data
  • Links, scripts, styles
  • 路径解析/序列化
  • validateSearch
  • loader
    、
    beforeLoad
  • 路由上下文、静态数据
  • 链接、脚本、样式

What Gets Split (Non-Critical)

会被分割的内容(非关键代码)

  • component
  • errorComponent
  • pendingComponent
  • notFoundComponent
The
loader
is NOT split by default. It is already async, so splitting it adds a double async cost: fetch the chunk, then execute the loader. Only split the loader if you have a specific reason.
  • component
  • errorComponent
  • pendingComponent
  • notFoundComponent
loader
默认不会被分割。它本身已经是异步的,因此分割它会带来双重异步开销:先获取代码块,再执行 loader。除非有特殊原因,否则不要分割 loader。

Setup: Automatic Code Splitting

配置:自动代码分割

Enable
autoCodeSplitting: true
in the bundler plugin. This is the recommended approach.
ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    // TanStack Router plugin MUST come before the framework plugin
    tanstackRouter({
      autoCodeSplitting: true,
    }),
    react(),
  ],
})
With this enabled, route files are automatically transformed. Components are split into separate chunks; loaders stay in the main bundle. No
.lazy.tsx
files needed.
tsx
// src/routes/posts.tsx — everything in one file, splitting is automatic
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'

export const Route = createFileRoute('/posts')({
  loader: fetchPosts,
  component: PostsComponent,
})

// NOT exported — this is critical for automatic code splitting to work
function PostsComponent() {
  const posts = Route.useLoaderData()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
在打包器插件中启用
autoCodeSplitting: true
,这是推荐的实现方式。
ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    // TanStack Router plugin MUST come before the framework plugin
    tanstackRouter({
      autoCodeSplitting: true,
    }),
    react(),
  ],
})
启用该配置后,路由文件会被自动转换。组件会被分割到独立的代码块中,loader 则保留在主包里,无需编写
.lazy.tsx
文件。
tsx
// src/routes/posts.tsx — everything in one file, splitting is automatic
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'

export const Route = createFileRoute('/posts')({
  loader: fetchPosts,
  component: PostsComponent,
})

// NOT exported — this is critical for automatic code splitting to work
function PostsComponent() {
  const posts = Route.useLoaderData()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Manual Splitting with
.lazy.tsx

使用
.lazy.tsx
手动分割

If you cannot use automatic code splitting (e.g. CLI-only, no bundler plugin), split manually into two files:
tsx
// src/routes/posts.tsx — critical route config only
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'

export const Route = createFileRoute('/posts')({
  loader: fetchPosts,
})
tsx
// src/routes/posts.lazy.tsx — non-critical (lazy-loaded)
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Use getRouteApi to access typed hooks without importing Route
  return <div>Posts</div>
}
createLazyFileRoute
supports only:
component
,
errorComponent
,
pendingComponent
,
notFoundComponent
.
如果你无法使用自动代码分割(例如仅使用 CLI、没有打包器插件),可以手动拆分为两个文件:
tsx
// src/routes/posts.tsx — critical route config only
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'

export const Route = createFileRoute('/posts')({
  loader: fetchPosts,
})
tsx
// src/routes/posts.lazy.tsx — non-critical (lazy-loaded)
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Use getRouteApi to access typed hooks without importing Route
  return <div>Posts</div>
}
createLazyFileRoute
仅支持配置:
component
、
errorComponent
、
pendingComponent
、
notFoundComponent
。

Virtual Routes

虚拟路由

If splitting leaves the critical route file empty, delete it entirely. A virtual route is auto-generated in
routeTree.gen.ts
:
tsx
// src/routes/about.lazy.tsx — no about.tsx needed
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/about')({
  component: () => <h1>About Us</h1>,
})
如果分割后关键路由文件为空,可以直接删除它。系统会在
routeTree.gen.ts
中自动生成虚拟路由:
tsx
// src/routes/about.lazy.tsx — no about.tsx needed
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/about')({
  component: () => <h1>About Us</h1>,
})

Code-Based Splitting

基于代码的分割

For code-based (non-file-based) routing, use
createLazyRoute
and the
.lazy()
method:
tsx
// src/posts.lazy.tsx
import { createLazyRoute } from '@tanstack/react-router'

export const Route = createLazyRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  return <div>Posts</div>
}
tsx
// src/app.tsx
import { createRoute } from '@tanstack/react-router'

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/posts',
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
对于基于代码(非文件式)的路由,请使用
createLazyRoute
和
.lazy()
方法:
tsx
// src/posts.lazy.tsx
import { createLazyRoute } from '@tanstack/react-router'

export const Route = createLazyRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  return <div>Posts</div>
}
tsx
// src/app.tsx
import { createRoute } from '@tanstack/react-router'

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/posts',
}).lazy(() => import('./posts.lazy').then((d) => d.Route))

Accessing Typed Hooks in Split Files:
getRouteApi

在分割文件中访问类型化钩子:
getRouteApi

When your component lives in a separate file, use
getRouteApi
to get typed access to route hooks without importing the Route object:
tsx
// src/routes/posts.lazy.tsx
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/posts')

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  const posts = routeApi.useLoaderData()
  const { page } = routeApi.useSearch()
  const params = routeApi.useParams()
  const context = routeApi.useRouteContext()
  return <div>Posts page {page}</div>
}
getRouteApi
provides:
useLoaderData
,
useLoaderDeps
,
useMatch
,
useParams
,
useRouteContext
,
useSearch
.
当你的组件位于独立文件中时,可以使用
getRouteApi
获取路由钩子的类型化访问能力,无需导入 Route 对象:
tsx
// src/routes/posts.lazy.tsx
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/posts')

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  const posts = routeApi.useLoaderData()
  const { page } = routeApi.useSearch()
  const params = routeApi.useParams()
  const context = routeApi.useRouteContext()
  return <div>Posts page {page}</div>
}
getRouteApi
提供以下钩子:
useLoaderData
、
useLoaderDeps
、
useMatch
、
useParams
、
useRouteContext
、
useSearch
。

Per-Route Split Overrides:
codeSplitGroupings

单路由分割覆盖:
codeSplitGroupings

Override split behavior for a specific route by adding
codeSplitGroupings
directly in the route file:
tsx
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { loadPostsData } from './-heavy-posts-utils'

export const Route = createFileRoute('/posts')({
  // Bundle loader and component together for this route
  codeSplitGroupings: [['loader', 'component']],
  loader: () => loadPostsData(),
  component: PostsComponent,
})

function PostsComponent() {
  const data = Route.useLoaderData()
  return <div>{data.title}</div>
}
直接在路由文件中添加
codeSplitGroupings
,可以覆盖特定路由的分割行为:
tsx
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { loadPostsData } from './-heavy-posts-utils'

export const Route = createFileRoute('/posts')({
  // Bundle loader and component together for this route
  codeSplitGroupings: [['loader', 'component']],
  loader: () => loadPostsData(),
  component: PostsComponent,
})

function PostsComponent() {
  const data = Route.useLoaderData()
  return <div>{data.title}</div>
}

Global Split Configuration

全局分割配置

defaultBehavior
— Change Default Groupings

defaultBehavior
— 修改默认分组

ts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      autoCodeSplitting: true,
      codeSplittingOptions: {
        defaultBehavior: [
          // Bundle all UI components into one chunk
          [
            'component',
            'pendingComponent',
            'errorComponent',
            'notFoundComponent',
          ],
        ],
      },
    }),
  ],
})
ts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      autoCodeSplitting: true,
      codeSplittingOptions: {
        defaultBehavior: [
          // Bundle all UI components into one chunk
          [
            'component',
            'pendingComponent',
            'errorComponent',
            'notFoundComponent',
          ],
        ],
      },
    }),
  ],
})

splitBehavior
— Programmatic Per-Route Logic

splitBehavior
— 编程式单路由逻辑

ts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      autoCodeSplitting: true,
      codeSplittingOptions: {
        splitBehavior: ({ routeId }) => {
          if (routeId.startsWith('/posts')) {
            return [['loader', 'component']]
          }
          // All other routes use defaultBehavior
        },
      },
    }),
  ],
})
ts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      autoCodeSplitting: true,
      codeSplittingOptions: {
        splitBehavior: ({ routeId }) => {
          if (routeId.startsWith('/posts')) {
            return [['loader', 'component']]
          }
          // All other routes use defaultBehavior
        },
      },
    }),
  ],
})

Precedence Order

优先级顺序

  1. Per-route
    codeSplitGroupings
    (highest)
  2. splitBehavior
    function
  3. defaultBehavior
    option (lowest)
  1. 单路由
    codeSplitGroupings
    (最高)
  2. splitBehavior
    函数
  3. defaultBehavior
    配置项(最低)

Common Mistakes

常见错误

1. HIGH: Exporting component functions prevents code splitting

1. 高危:导出组件函数会导致代码分割失效

tsx
// WRONG — export puts PostsComponent in the main bundle
export function PostsComponent() {
  return <div>Posts</div>
}

// CORRECT — no export, function stays in the split chunk
function PostsComponent() {
  return <div>Posts</div>
}
tsx
// WRONG — export puts PostsComponent in the main bundle
export function PostsComponent() {
  return <div>Posts</div>
}

// CORRECT — no export, function stays in the split chunk
function PostsComponent() {
  return <div>Posts</div>
}

2. MEDIUM: Trying to code-split the root route

2. 中危:尝试对根路由进行代码分割

__root.tsx
does not support code splitting. It is always rendered regardless of the current route. Do not create
__root.lazy.tsx
.
__root.tsx
不支持代码分割。无论当前路由是什么,根路由始终会被渲染。请勿创建
__root.lazy.tsx
。

3. MEDIUM: Splitting the loader adds double async cost

3. 中危:分割 loader 会带来双重异步开销

tsx
// AVOID unless you have a specific reason
codeSplittingOptions: {
  defaultBehavior: [
    ['loader'], // Fetch chunk THEN execute loader = two network waterfalls
    ['component'],
  ],
}

// PREFERRED — loader stays in main bundle (default behavior)
codeSplittingOptions: {
  defaultBehavior: [
    ['component'],
    ['errorComponent'],
    ['notFoundComponent'],
  ],
}
tsx
// AVOID unless you have a specific reason
codeSplittingOptions: {
  defaultBehavior: [
    ['loader'], // Fetch chunk THEN execute loader = two network waterfalls
    ['component'],
  ],
}

// PREFERRED — loader stays in main bundle (default behavior)
codeSplittingOptions: {
  defaultBehavior: [
    ['component'],
    ['errorComponent'],
    ['notFoundComponent'],
  ],
}

4. HIGH: Importing Route in code-split files for typed hooks

4. 高危:在代码分割文件中导入 Route 以使用类型化钩子

tsx
// WRONG — importing Route pulls route config into the lazy chunk
import { Route } from './posts.tsx'
const data = Route.useLoaderData()

// CORRECT — getRouteApi gives typed hooks without pulling in the route
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
tsx
// WRONG — importing Route pulls route config into the lazy chunk
import { Route } from './posts.tsx'
const data = Route.useLoaderData()

// CORRECT — getRouteApi gives typed hooks without pulling in the route
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()

Cross-References

相关参考

  • router-core/data-loading — Loader splitting decisions affect data loading performance. Splitting the loader adds latency before data can be fetched.
  • router-core/type-safety —
    getRouteApi
    is the type-safe way to access hooks from split files.
  • router-core/data-loading — Loader 分割决策会影响数据加载性能。分割 loader 会增加数据获取前的延迟。
  • router-core/type-safety —
    getRouteApi
    是从分割文件中访问钩子的类型安全方式。