SSR 中为什么不能直接访问 window、document?客户端专属代码怎么处理?
简化版
SSR 代码会在服务端执行,服务端没有浏览器的 window、document、localStorage 等对象,直接访问会报错或造成水合不一致。处理方式是把浏览器 API 放到客户端生命周期、动态导入、客户端组件或环境判断中,并保证服务端首屏 HTML 与客户端首次渲染一致。
详细版
错误示例:
const width = window.innerWidth // SSR 阶段会报 window is not defined
React 中常见写法是放到 useEffect:
function Width() {
const [width, setWidth] = useState(null)
useEffect(() => setWidth(window.innerWidth), [])
return <span>{width ?? '-'}</span>
}
Next.js App Router 中也可以用 'use client' 标记客户端组件。面试重点是:不要只用 typeof window !== 'undefined' 粗暴包住,还要考虑服务端和客户端初始渲染是否一致。
完整版教学
一、SSR 有两个执行环境
同一份组件代码在 SSR 应用中可能执行两次:一次在服务端生成 HTML,一次在浏览器 hydration 或后续交互时执行。服务端是 Node 或 Edge 环境,浏览器才有 DOM。
Server render: 无 window/document/localStorage
Client hydrate: 有 window/document/localStorage
记忆钩子:SSR 组件先在“没有浏览器的地方”跑一遍,再到浏览器里接管。
二、直接访问 window 的问题
function Page() {
const theme = localStorage.getItem('theme')
return <div>{theme}</div>
}
这段代码在服务端会因为 localStorage 不存在而报错。即使加判断,也可能导致服务端输出和客户端第一次输出不同,从而产生 hydration mismatch。
例如服务端渲染 light,客户端首次读取 localStorage 得到 dark,React 会发现文本不一致。轻则警告,重则局部重新渲染。
三、把副作用放到客户端生命周期
function ThemeLabel() {
const [theme, setTheme] = useState('light')
useEffect(() => {
setTheme(localStorage.getItem('theme') || 'light')
}, [])
return <span>{theme}</span>
}
useEffect 不会在服务端执行,所以适合访问浏览器 API。代价是首屏 HTML 使用默认值,客户端 hydration 后再更新。对于主题这类会影响视觉的内容,还要考虑闪烁问题,可以通过内联脚本或 Cookie 提前把初始状态传给服务端。
四、客户端组件和动态导入
在 Next.js App Router 中,使用浏览器 API 的组件需要进入客户端边界:
'use client'
export function Chart() {
useEffect(() => {
console.log(window.devicePixelRatio)
}, [])
return <canvas />
}
对于依赖浏览器环境的第三方库,可以动态导入并关闭 SSR:
const Chart = dynamic(() => import('./Chart'), { ssr: false })
这会让组件只在客户端渲染,避免服务端执行报错。代价是该部分没有服务端 HTML,SEO 和首屏可见性可能下降。
五、环境判断不是万能药
const isBrowser = typeof window !== 'undefined'
环境判断可以避免服务端报错,但不能自动解决首屏一致性。若服务端渲染 null,客户端首次渲染真实内容,仍可能水合不一致或布局跳动。
| 方案 | 解决报错 | 解决一致性 | 代价 |
|---|---|---|---|
typeof window | 是 | 不一定 | 容易散落 |
useEffect | 是 | 首次一致较容易 | 客户端二次更新 |
| 动态导入关闭 SSR | 是 | 跳过服务端部分 | 首屏和 SEO 变差 |
| Cookie 注入初始状态 | 是 | 较好 | 服务端复杂度增加 |
六、第三方库怎么处理
很多图表、地图、富文本库在模块顶层就访问 window。这类库即使你在组件里加判断,也可能在 import 时已经报错。
import chartLib -> 顶层访问 window -> SSR 构建或运行时报错
解决方式包括动态导入、只在 useEffect 内导入、选择支持 SSR 的库,或把它封装为客户端组件。不要让服务端入口直接 import 浏览器专属库。
七、常见误区与追问
- 误区:SSR 中加
typeof window就万事大吉。 它只避免报错,不保证 hydration 一致。 - 误区:
useEffect会在服务端执行。 Effect 只在客户端提交后执行。 - 误区:关闭 SSR 没有代价。 该组件失去服务端 HTML,可能影响首屏和 SEO。
- 追问:为什么第三方库 import 就报错? 它可能在模块顶层访问浏览器 API。
- 追问:主题闪烁怎么解决? 用 Cookie、服务端注入初始主题或首屏内联脚本减少不一致。
- 追问:哪些对象属于浏览器专属?
window、document、navigator、localStorage、DOM API 等。
八、加强记忆
SSR 客户端专属代码要按“环境、时机、一致性”记:服务端没有浏览器对象,所以访问时机要放到客户端;但避免报错还不够,服务端 HTML 和客户端首次渲染要尽量一致。能 SSR 的内容保留 SSR,纯浏览器组件再用客户端组件或动态导入隔离。