ByteNoteByteNote

字节笔记本

2026年5月3日

TanStack Start - Hydration Errors 水合错误

API中转
¥120

水合错误发生在服务端 HTML 与客户端水合期间的渲染不匹配时。本文介绍理解和解决这类问题的策略。

为什么会发生水合错误

常见原因

原因示例
Intl(locale/time zone)日期格式化因环境而异
Date.now()时间戳不同
随机 ID服务端和客户端生成不同 ID
响应式逻辑仅在客户端可见的元素
功能开关环境特定配置
用户偏好存储在客户端的设置

策略 1 - 使服务端和客户端匹配

在服务端选择确定性 locale/time zone 并在客户端使用相同的。真实来源:cookie(首选)或 Accept-Language 头。

tsx
const localeTzMiddleware = createMiddleware().server(async ({ next }) => {
  const header = getRequestHeader('accept-language')
  const headerLocale = header?.split(',')[0] || 'en-US'
  const cookieLocale = getCookie('locale')
  const cookieTz = getCookie('tz')

  const locale = cookieLocale || headerLocale
  const timeZone = cookieTz || 'UTC'

  return next({ context: { locale, timeZone } })
})

策略 2 - 让客户端告诉你它的环境

在首次访问时,使用客户端时区设置 cookie。SSR 在此之前使用 UTC

tsx
function SetTimeZoneCookie() {
  React.useEffect(() => {
    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
    document.cookie = `tz=${tz}; path=/; max-age=31536000`
  }, [])
  return null
}

export function AppBoot() {
  return (
    <ClientOnly fallback={null}>
      <SetTimeZoneCookie />
    </ClientOnly>
  )
}

策略 3 - 使其仅客户端

将不稳定的 UI 包装在 <ClientOnly> 中以避免 SSR 和不匹配:

tsx
function RelativeTime({ ts }: { ts: number }) {
  const [time, setTime] = React.useState('')

  React.useEffect(() => {
    const formatter = new Intl.RelativeTimeFormat('en')
    setTime(formatter.format(Math.floor((Date.now() - ts) / 1000), 'seconds'))
  }, [ts])

  return <span>{time}</span>
}

export function MyComponent() {
  return (
    <ClientOnly fallback={<span>—</span>}>
      <RelativeTime ts={Date.now()} />
    </ClientOnly>
  )
}

策略 4 - 为路由禁用或限制 SSR

tsx
export const Route = createFileRoute('/unstable')({
  ssr: 'data-only', // 或 false
  component: () => <ExpensiveViz />,
})

策略 5 - 最后手段的抑制

对于小的、已知不同的节点,可以使用 React 的 suppressHydrationWarning

tsx
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>

suppressHydrationWarning 应该谨慎使用,仅用于你知道差异是可接受的情况。

策略对比

策略适用场景复杂度性能影响
匹配服务端/客户端日期、时间、国际化
客户端设置环境时区检测小(首次访问)
ClientOnly动态 UI、浏览器 API中(延迟渲染)
选择性 SSR复杂可视化中(无 SSR)
抑制警告小的已知差异

最佳实践

  1. 优先使用确定性数据 - useId、稳定 ID、服务端计算的值
  2. 延迟客户端特定功能 - useEffect 中设置状态
  3. 使用 CSS 处理响应式 - 媒体查询而非 JS 检测
  4. 谨慎使用 suppressHydrationWarning - 仅用于小的已知差异

策略决策树

text
是否需要 SSR?
├─ 是 → 能使服务端和客户端匹配?
│        ├─ 是 → 策略 1: 匹配环境
│        └─ 否 → 能接受客户端设置环境?
│                 ├─ 是 → 策略 2: 客户端设置
│                 └─ 否 → 策略 4: 选择性 SSR
└─ 否 → 策略 3: ClientOnly
分享: