首页 / TanStack 生态入门教程 / SSR 与渲染模式

TanStack 生态入门教程

SSR 与渲染模式

本教程共 38 篇 · 第 24 篇 · 更新于 2026-07-27 · 约 17 分钟阅读

TanStackTanStack 生态入门教程TanStack StartSSRhydrationSPA预渲染ISR

24. SSR 与渲染模式

本节目标:搞懂 TanStack Start 的各种渲染策略。学会 SSR 基础与 hydration、selective-ssr 按路由控制、SPA 模式、静态预渲染、ISR 增量静态再生、deferred-hydration 延迟注水。学完你能为每个页面选最合适的渲染方式,平衡性能、SEO 和开发体验。

24.1 SSR 与 hydration:基础原理

什么是 SSR

SSR(Server-Side Rendering)是服务端渲染:用户请求页面时,服务器执行 React 组件,生成完整 HTML 发给浏览器。用户不用等 JavaScript 下载执行就能看到页面内容。

用户请求 → 服务器执行组件 → 生成 HTML → 浏览器显示
                                         ↓ (同时)
                                    下载 JS → hydration → 可交互

什么是 hydration

浏览器拿到 HTML 后,页面只是”看得见但点不了”。JavaScript 下载执行后,React 把 HTML 变成可交互的应用,这个过程叫 hydration(注水)

hydration 做的事:

  1. 读取服务端生成的 HTML
  2. 在内存中重建组件树
  3. 把事件处理器挂到 DOM 上
  4. 替换服务端 HTML 为客户端可控的 DOM
Note

TanStack Start 默认所有路由都做 SSR。beforeLoadloader 在服务端执行,组件在服务端渲染,HTML 发给客户端后 hydration。

hydration 不匹配问题

如果服务端和客户端渲染的内容不一致,React 会报 hydration mismatch 警告。

// ❌ 服务端和客户端渲染不同内容
function CurrentTime() {
  return <div>{new Date().toLocaleString()}</div>
  // 服务端渲染时是时间 A,客户端 hydration 时是时间 B,不一致!
}

// ✅ 用 useEffect 确保 hydration 后才渲染动态内容
function CurrentTime() {
  const [time, setTime] = useState<string>()

  useEffect(() => {
    setTime(new Date().toLocaleString())
  }, [])

  return <div>{time || '加载中...'}</div>
}
Warning

服务端和客户端时间不同、随机数不同、window/document 在服务端不存在,都会导致 hydration mismatch。用 useHydrated hook 或 useEffect 规避。

24.2 selective-ssr:按路由控制 SSR

不是所有路由都适合 SSR。有些路由的 loader 用了浏览器 API(localStorage),有些组件依赖 canvas。TanStack Start 的 Selective SSR 让你按路由控制。

三种 SSR 模式

模式beforeLoad/loader组件渲染适用场景
ssr: true服务端执行服务端渲染默认,需要 SEO
ssr: false客户端执行客户端渲染依赖浏览器 API
ssr: 'data-only'服务端执行客户端渲染数据要 SEO,组件不要

ssr: true(默认)

export const Route = createFileRoute('/posts/$postId')({
  ssr: true, // 默认值,不写也行
  loader: () => fetchPost(),
  component: PostDetail,
})

服务端执行 loader 获取数据,渲染组件生成 HTML。客户端拿到带数据的 HTML,直接显示。

ssr: false

export const Route = createFileRoute('/dashboard')({
  ssr: false,
  loader: () => {
    // 只在客户端执行
    const token = localStorage.getItem('token')
    return fetchDashboardData(token)
  },
  component: Dashboard,
})

beforeLoadloader 不在服务端执行,组件也不在服务端渲染。用户首次访问会看到 pendingComponent(或空白),客户端 JS 加载后才渲染。

ssr: ‘data-only’

export const Route = createFileRoute('/editor')({
  ssr: 'data-only',
  loader: () => fetchEditorData(),
  component: Editor,
})

数据在服务端加载(SEO 友好),但组件不在服务端渲染。适合数据需要被搜索引擎索引,但组件依赖浏览器 API 的场景。

函数式配置

运行时动态决定:

export const Route = createFileRoute('/docs/$docType/$docId')({
  ssr: ({ params, search }) => {
    // 某些文档类型依赖浏览器 API,不做 SSR
    if (params.docType === 'sheet') {
      return false
    }
    // 带特定搜索参数时只 SSR 数据
    if (search.preview === true) {
      return 'data-only'
    }
    return true
  },
})

继承规则

子路由继承父路由的 SSR 配置,但只能变得更严格:

true → 'data-only' → false(可以)
false → true(不行,不能变得更宽松)
root (ssr: true)
  └─ posts (ssr: false)
       └─ $postId (ssr: true)  ← 实际还是 false,继承自父级
Tip

全局关闭 SSR 用 defaultSsr: false,但 <html> 外壳仍然在服务端渲染(用 shellComponent 配置)。

24.3 SPA 模式:纯客户端

有些应用不需要 SSR:内部工具、后台管理、不需要 SEO 的应用。Start 的 SPA 模式 完全关闭 SSR。

开启 SPA 模式

// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    tanstackStart({
      spa: {
        enabled: true,
      },
    }),
    viteReact(),
  ],
})

SPA 模式怎么工作

构建时会额外执行预渲染步骤:

  1. 只预渲染根路由(生成 HTML 外壳)
  2. 路由匹配的位置渲染 pendingComponent
  3. 生成 /_shell.html 文件
  4. 配置所有 404 请求重写到 /_shell.html

用户访问任何 URL 都会拿到同一个 HTML 外壳,客户端 JS 加载后由路由器接管。

SPA 模式的优缺点

优点

  • 部署简单:CDN 能托管静态文件就行
  • 成本低:不需要服务器运行
  • 少出错:没有 hydration 问题

缺点

  • 首屏慢:所有 JS 下载执行后才能看到内容
  • SEO 差:搜索引擎可能抓不到内容
Note

SPA 模式不等于不能用 Server Functions 和 Server Routes。只是首屏 HTML 不含渲染内容,但可以配合服务端功能使用。

部署重定向

SPA 部署到 CDN 需要配置重定向,确保所有路径都返回 /_shell.html

# Netlify _redirects
/_serverFn/* /_serverFn/:splat 200
/api/* /api/:splat 200
/* /_shell.html 200

24.4 静态预渲染:构建时生成 HTML

静态预渲染(Static Prerendering) 在构建时把页面渲染成静态 HTML 文件。用户请求时直接返回 HTML,不用服务端实时渲染。

配置预渲染

// vite.config.ts
export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: {
        enabled: true,
        // 自动发现静态路由
        autoStaticPathsDiscovery: true,
        // 从预渲染的页面中提取链接,继续预渲染
        crawlLinks: true,
        // 并发数
        concurrency: 14,
        // 重试次数
        retryCount: 2,
        // 过滤不需要预渲染的页面
        filter: ({ path }) => !path.startsWith('/admin'),
      },
    }),
    viteReact(),
  ],
})

自动路由发现

Start 自动发现可以预渲染的路由:

  • 静态路由(如 /about):自动预渲染
  • 动态路由(如 /posts/$postId):需要参数值,不会自动发现
  • 布局路由:不渲染独立页面,跳过

链接爬取

crawlLinks: true 时,预渲染 / 后会提取页面里的链接,继续预渲染链接指向的页面。从首页开始能预渲染大部分页面。

指定特定页面

tanstackStart({
  prerender: {
    enabled: true,
    crawlLinks: true,
  },
  pages: [
    {
      path: '/landing/special-campaign',
      prerender: { enabled: true, outputPath: '/landing/special-campaign/index.html' },
    },
  ],
})
Tip

预渲染适合内容不常变的页面:博客文章、营销页、文档。动态内容(用户数据、实时数据)不适合。

24.5 ISR:增量静态再生

ISR(Incremental Static Regeneration) 是预渲染 + 定期更新。页面在构建时预渲染,CDN 缓存,过期后后台重新生成。

Start 的 ISR 不用框架特有的机制,而是用标准 HTTP 缓存头配合 CDN。

基本用法

在路由上设置 headers

// src/routes/blog/posts/$postId.tsx
export const Route = createFileRoute('/blog/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  headers: () => ({
    // CDN 缓存 1 小时,过期后 24 小时内返回旧内容同时后台刷新
    'Cache-Control':
      'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',
  }),
})

Cache-Control 指令

指令含义
public任何缓存都能存(CDN、浏览器)
max-age=36001 小时内视为新鲜
s-maxage=3600覆盖 CDN 的 max-age
stale-while-revalidate=86400过期后 24 小时内返回旧内容,同时后台刷新
private只浏览器能缓存(认证数据)
immutable内容永不变(哈希命名的资源)

多层缓存策略

CDN 缓存 + 客户端缓存配合:

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => fetchPost(params.postId),
  // CDN 缓存(通过 headers)
  headers: () => ({
    'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
  }),
  // 客户端缓存(通过 Router)
  staleTime: 60_000,  // 客户端 60 秒内视为新鲜
  gcTime: 5 * 60_000, // 客户端内存保留 5 分钟
})

三层缓存:

  1. CDN 边缘:1 小时缓存,过期后 24 小时 stale-while-revalidate
  2. 客户端 Router:60 秒新鲜,5 分钟内存
  3. 服务器:CDN 缓存未命中时回源

按需重新验证

内容更新后需要立即刷新缓存,调 CDN 的 purge API:

export const Route = createFileRoute('/api/revalidate')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const { path, secret } = await request.json()

        // 验证密钥
        if (secret !== process.env.REVALIDATE_SECRET) {
          return Response.json({ error: 'Invalid token' }, { status: 401 })
        }

        // 调 CDN API 清除缓存
        await fetch(
          `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
          {
            method: 'POST',
            headers: { Authorization: `Bearer ${CF_API_TOKEN}` },
            body: JSON.stringify({ files: [`https://yoursite.com${path}`] }),
          },
        )

        return Response.json({ revalidated: true })
      },
    },
  },
})

常见 ISR 场景

// 博客文章:缓存 1 小时,7 天 stale
headers: () => ({
  'Cache-Control': 'public, max-age=3600, stale-while-revalidate=604800',
})

// 电商产品:库存变化快,缓存 5 分钟
headers: () => ({
  'Cache-Control': 'public, max-age=300, stale-while-revalidate=3600',
})

// 营销页面:内容稳定,缓存 1 天
headers: () => ({
  'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800',
})

// 用户仪表盘:私有数据,不 CDN 缓存
headers: () => ({
  'Cache-Control': 'private, max-age=60',
})
Warning

用户相关数据必须用 private,不能用 public。否则 CDN 会把一个用户的数据缓存了给另一个用户。

24.6 deferred-hydration:延迟注水

Note

Deferred hydration 目前是实验性功能,API 可能变化。

为什么要延迟注水

SSR 让用户快速看到 HTML,但 hydration 要下载执行所有 JavaScript。页面下方的评论区、推荐栏,用户暂时不需要交互,却要为它们付出 hydration 成本。

Deferred Hydration(延迟注水)让你标记部分内容”先不交互”,等需要时再注水。

基本用法

import { Hydrate } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'

export function ProductPage() {
  return (
    <>
      <ProductHero />
      <BuyBox />

      {/* 评论区:滚动到可视区域时才注水 */}
      <Hydrate when={visible({ rootMargin: '400px' })}>
        <Reviews />
      </Hydrate>
    </>
  )
}

服务端仍然渲染 <Reviews /> 的 HTML(用户能看到内容),但客户端不会立即注水。当评论区进入可视区域 400px 范围内时,才加载 JavaScript 并注水。

注水策略

策略行为
load()应用启动时就注水
idle()浏览器空闲时注水
visible()进入可视区域时注水
media()媒体查询匹配时注水
interaction()用户交互时注水(点击、聚焦等)
condition()条件为真时注水
never()永不注水(保持静态 HTML)

常见用法

可视区域触发(首屏下方的评论区):

<Hydrate when={visible({ rootMargin: '800px' })}>
  <Reviews />
</Hydrate>

用户交互触发(昂贵控件,需要时才激活):

<Hydrate when={interaction({ events: ['focusin', 'click'] })}>
  <ReviewFilters />
</Hydrate>

空闲时注水(小组件,不急但也不推迟太久):

<Hydrate when={idle()} split={false}>
  <SmallBadge />
</Hydrate>

永不注水(纯展示内容,不需要交互):

<Hydrate when={never()}>
  <StaticBadges />
</Hydrate>

代码分割

Hydrate 默认把子组件代码拆到单独的 chunk,注水时才加载:

// 默认:split=true,代码拆分
<Hydrate when={visible()}>
  <HeavyWidget />
</Hydrate>

// 关闭拆分:只延迟注水,不拆代码
<Hydrate when={idle()} split={false}>
  <SmallWidget />
</Hydrate>

预加载

在注水前提前加载代码,让注水触发时更快:

import { idle, visible } from '@tanstack/react-start/hydration'

// 可视区域触发注水,但空闲时就预加载代码
<Hydrate when={visible({ rootMargin: '200px' })} prefetch={idle()}>
  <Reviews />
</Hydrate>

嵌套边界

export function ProductPage() {
  return (
    <>
      <ProductHero />
      <BuyBox />

      <Hydrate when={visible({ rootMargin: '600px' })}>
        <section>
          <h2>评论</h2>
          <ReviewsList />

          <Hydrate when={interaction({ events: ['focusin', 'click'] })}>
            <ReviewFilters />
          </Hydrate>

          <Hydrate when={interaction({ events: 'click' })}>
            <WriteReviewForm />
          </Hydrate>
        </section>
      </Hydrate>
    </>
  )
}

父级先注水,子级才能注水。滚动到附近先激活评论区,用户点击筛选器再激活筛选功能。

Tip

好的延迟注水候选:首屏下方的评论、推荐、地图、图表、轮播。差的候选:主导航、搜索框、加购按钮、首屏表单—这些用户可能立刻就要用。

24.7 渲染模式选择指南

模式首屏速度SEO实时性部署成本适用场景
SSR(默认)高(需服务器)通用,动态内容
selective-ssr部分混合场景
SPA 模式低(CDN)后台管理
静态预渲染最快低(CDN)博客、文档
ISR电商、新闻
deferred-hydration复杂页面优化

实际项目中可以组合使用:

  • 首页用预渲染(最快首屏)
  • 博客用 ISR(定期更新)
  • 用户仪表盘用 ssr: 'data-only'(数据 SEO,组件客户端渲染)
  • 评论区用 deferred-hydration(延迟注水)

24.8 小结

这一章覆盖了 Start 的所有渲染模式:

  • SSR 基础:服务端渲染 HTML,客户端 hydration 变可交互
  • hydration mismatch:服务端客户端不一致导致,用 useEffectuseHydrated 规避
  • selective-ssr:按路由控制 ssr: true/false/'data-only',子路由只能更严格
  • SPA 模式:完全关闭 SSR,生成 /_shell.html 外壳,部署到 CDN
  • 静态预渲染:构建时生成 HTML,自动发现路由,爬取链接
  • ISR:用标准 HTTP 缓存头配合 CDN,stale-while-revalidate 后台刷新
  • deferred-hydration:标记部分内容延迟注水,visible/interaction/idle/never 策略控制

下一章讲部署与生产环境:怎么把 Start 应用部署到各种平台。