← 返回题目列表

SSR 中为什么不能直接访问 window、document?客户端专属代码怎么处理?

高频 中等 第 10 / 25 题 更新于 2026/07/29
SSRwindowdocument客户端边界

简化版

SSR 代码会在服务端执行,服务端没有浏览器的 windowdocumentlocalStorage 等对象,直接访问会报错或造成水合不一致。处理方式是把浏览器 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、服务端注入初始主题或首屏内联脚本减少不一致。
  • 追问:哪些对象属于浏览器专属? windowdocumentnavigatorlocalStorage、DOM API 等。

八、加强记忆

SSR 客户端专属代码要按“环境、时机、一致性”记:服务端没有浏览器对象,所以访问时机要放到客户端;但避免报错还不够,服务端 HTML 和客户端首次渲染要尽量一致。能 SSR 的内容保留 SSR,纯浏览器组件再用客户端组件或动态导入隔离。