TanStack Start 中文文档
渲染

选择性服务端渲染(Selective SSR)

什么是选择性 SSR?

在 TanStack Start 中,匹配初始请求的路由默认会在服务器上渲染。这意味着 beforeLoadloader 会在服务器上执行,然后渲染路由组件。生成的 HTML 被发送给客户端,客户端把标记水合成一个完全可交互的应用。

然而,有些情况下你可能想对某些路由或所有路由禁用 SSR,比如:

  • beforeLoadloader 需要浏览器专属 API(例如 localStorage)。
  • 当路由组件依赖浏览器专属 API(例如 canvas)。

TanStack Start 的选择性 SSR(Selective SSR)特性让你配置:

  • 哪些路由应该在服务器上执行 beforeLoadloader
  • 哪些路由组件应该在服务器上渲染。

与 SPA 模式相比如何?

TanStack Start 的 SPA 模式完全禁用 beforeLoadloader 的服务端执行,也禁用路由组件的服务端渲染。选择性 SSR 允许你按路由配置服务端处理,可以是静态的,也可以是动态的。

配置

你可以用 ssr 属性控制路由在初始服务端请求中的处理方式。如果未设置该属性,默认值为 true。你可以用 createStart 中的 defaultSsr 选项改变这个默认值:

// src/start.ts
import { createStart } from '@tanstack/react-start'

export const startInstance = createStart(() => ({
  // Disable SSR by default
  defaultSsr: false,
}))

ssr: true

这是默认行为(除非另行配置)。在初始请求时,它会:

  • 在服务器上运行 beforeLoad,并把生成的 context 发送给客户端。
  • 在服务器上运行 loader,并把加载器数据发送给客户端。
  • 在服务器上渲染组件,并把 HTML 标记发送给客户端。
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  ssr: true,
  beforeLoad: () => {
    console.log('Executes on the server during the initial request')
    console.log('Executes on the client for subsequent navigation')
  },
  loader: () => {
    console.log('Executes on the server during the initial request')
    console.log('Executes on the client for subsequent navigation')
  },
  component: () => <div>This component is rendered on the server</div>,
})

ssr: false

这会禁用服务端的:

  • 路由 beforeLoadloader 的执行。
  • 路由组件的渲染。
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  ssr: false,
  beforeLoad: () => {
    console.log('Executes on the client during hydration')
  },
  loader: () => {
    console.log('Executes on the client during hydration')
  },
  component: () => <div>This component is rendered on the client</div>,
})

ssr: 'data-only'

这个混合选项会:

  • 在服务器上运行 beforeLoad,并把生成的 context 发送给客户端。
  • 在服务器上运行 loader,并把加载器数据发送给客户端。
  • 禁用路由组件的服务端渲染。
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  ssr: 'data-only',
  beforeLoad: () => {
    console.log('Executes on the server during the initial request')
    console.log('Executes on the client for subsequent navigation')
  },
  loader: () => {
    console.log('Executes on the server during the initial request')
    console.log('Executes on the client for subsequent navigation')
  },
  component: () => <div>This component is rendered on the client</div>,
})

函数形式

为了获得更多灵活性,你可以使用 ssr 属性的函数形式,在运行时决定是否对路由做 SSR:

// src/routes/docs/$docType/$docId.tsx
export const Route = createFileRoute('/docs/$docType/$docId')({
  validateSearch: z.object({ details: z.boolean().optional() }),
  ssr: ({ params, search }) => {
    if (params.status === 'success' && params.value.docType === 'sheet') {
      return false
    }
    if (search.status === 'success' && search.value.details) {
      return 'data-only'
    }
  },
  beforeLoad: () => {
    console.log('Executes on the server depending on the result of ssr()')
  },
  loader: () => {
    console.log('Executes on the server depending on the result of ssr()')
  },
  component: () => <div>This component is rendered on the client</div>,
})

ssr 函数只在初始请求期间运行于服务器上,并且会从客户端打包产物中剥离。

searchparams 在校验后以判别联合(discriminated union)的形式传入:

params:
    | { status: 'success'; value: Expand<ResolveAllParamsFromParent<TParentRoute, TParams>> }
    | { status: 'error'; error: unknown }
search:
    | { status: 'success'; value: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>> }
    | { status: 'error'; error: unknown }

如果校验失败,status 会是 errorerror 包含失败详情。否则,status 会是 successvalue 包含校验后的数据。

继承

在运行时,子路由会继承父级的选择性 SSR 配置。但继承的值只能变得更严格(即 true 变成 data-onlyfalsedata-only 变成 false)。例如:

root { ssr: undefined }
  posts { ssr: false }
     $postId { ssr: true }
  • root 默认为 ssr: true
  • posts 显式设置 ssr: false,所以 beforeLoadloader 都不会在服务器上运行,路由组件也不会在服务器上渲染。
  • $postId 设置了 ssr: true,但从父级继承了 ssr: false。因为继承的值只能变得更严格,ssr: true 不起作用,继承的 ssr: false 会保留。

另一个例子:

root { ssr: undefined }
  posts { ssr: 'data-only' }
     $postId { ssr: true }
       details { ssr: false }
  • root 默认为 ssr: true
  • posts 设置 ssr: 'data-only',所以 beforeLoadloader 在服务器上运行,但路由组件不在服务器上渲染。
  • $postId 设置了 ssr: true,但从父级继承了 ssr: 'data-only'
  • details 设置 ssr: false,所以 beforeLoadloader 都不会在服务器上运行,路由组件也不会在服务器上渲染。这里继承的值被变得更严格,因此 ssr: false 会覆盖继承的值。

译者注:继承只会更严格

选择性 SSR 的继承规则是「只能往更禁用方向走」:父级关掉了 SSR(false),子级就不能再打开(true 无效);父级是 data-only,子级可以降到 false,但不能升回 true。这是有意设计,保证嵌套路由树的行为可预测、不会出现「父级不渲染但子级渲染」的矛盾。

兜底渲染

对于第一个 ssr: falsessr: 'data-only' 的路由,服务器会把路由的 pendingComponent 作为兜底渲染。如果没有配置 pendingComponent,会渲染 defaultPendingComponent。如果两者都没配置,就不会渲染兜底。

在客户端水合期间,这个兜底至少会显示 minPendingMs(如果未配置则用 defaultPendingMinMs),即使路由没有定义 beforeLoadloader

如何禁用根路由的 SSR?

你可以禁用根路由组件的服务端渲染,但 <html> 外壳仍然需要在服务器上渲染。这个外壳通过 shellComponent 属性配置,它接收一个 children 属性。shellComponent 总是被 SSR,它分别包裹着根 component、根 errorComponent 或根 notFound 组件。

一个为路由组件禁用 SSR 的根路由最小配置是这样的:

import * as React from 'react'

import {
  HeadContent,
  Outlet,
  Scripts,
  createRootRoute,
} from '@tanstack/react-router'

export const Route = createRootRoute({
  shellComponent: RootShell,
  component: RootComponent,
  errorComponent: () => <div>Error</div>,
  notFoundComponent: () => <div>Not found</div>,
  ssr: false, // or `defaultSsr: false` on the router
})

function RootShell({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  )
}

function RootComponent() {
  return (
    <div>
      <h1>This component will be rendered on the client</h1>
      <Outlet />
    </div>
  )
}

On this page