字
字节笔记本
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) |
| 抑制警告 | 小的已知差异 | 低 | 无 |
最佳实践
- 优先使用确定性数据 - useId、稳定 ID、服务端计算的值
- 延迟客户端特定功能 - useEffect 中设置状态
- 使用 CSS 处理响应式 - 媒体查询而非 JS 检测
- 谨慎使用 suppressHydrationWarning - 仅用于小的已知差异
策略决策树
text
是否需要 SSR?
├─ 是 → 能使服务端和客户端匹配?
│ ├─ 是 → 策略 1: 匹配环境
│ └─ 否 → 能接受客户端设置环境?
│ ├─ 是 → 策略 2: 客户端设置
│ └─ 否 → 策略 4: 选择性 SSR
└─ 否 → 策略 3: ClientOnly分享: