服务端渲染与静态生成

SPA 通常采用客户端渲染,但在首屏速度和搜索引擎可见性要求较高的场景中,浏览器需要更早收到可展示的 HTML。SSR 和 SSG 将 HTML 的生成提前到请求期间或构建阶段,ISR 则为静态内容补充增量更新能力。这些策略的主要区别在于 HTML 的生成时机和缓存方式。

四种渲染策略

CSR:运行时渲染

CSR(Client-Side Rendering)把主要渲染工作交给浏览器。服务端提供应用入口和基础资源,浏览器加载 JavaScript、请求数据,再生成页面内容。

CSR 适合复杂交互,常见于后台、编辑器和控制台等登录后应用。它的部署和运行模型相对简单,但首屏需要等待脚本和数据,弱网体验与搜索引擎可见性也会受到影响。

SSR:请求时生成

SSR(Server-Side Rendering)在请求期间生成 HTML。服务端可以读取 Cookie、请求头、地区和权限等请求上下文,返回当前请求对应的页面。

SSR 适合依赖登录态、地理位置、权限或搜索词的页面。它需要服务端运行时,还要处理缓存、峰值流量、超时和渲染失败。

SSG:构建时生成

SSG(Static Site Generation)在构建阶段生成 HTML,部署后由静态服务器或 CDN 直接返回。

SSG 适合内容稳定、路由可枚举、更新频率可控的页面,例如文档、博客、官网和营销页。它的运行时成本低,但内容变化通常需要重新构建和发布。

ISR:静态生成与增量更新

ISR(Incremental Static Regeneration)在 SSG 的基础上增加失效和重新生成机制。它通常在构建阶段生成已知页面,也可以在首次访问时按需生成;生成结果会写入静态缓存,后续请求直接复用。缓存达到重新生成条件或被主动失效后,后续请求会触发页面重新生成。根据具体实现,当前请求可以等待并返回新页面,也可以先返回旧页面,再由服务端在后台更新缓存。

ISR 的重新生成虽然发生在服务端,但服务方式与 SSR 不同:SSR 通常为每个请求生成页面,ISR 只在首次访问或缓存需要更新时生成页面,其余请求复用缓存。因此,ISR 属于 SSG 的扩展,而不是 SSR。

ISR 适合生成结果可共享、内容允许短暂更新延迟,且不希望频繁重建整站的页面。实现时还需要处理重新生成失败和并发请求重复生成等问题。缓存时长、按路径失效和缓存标签等能力取决于框架与部署环境。

Hydration

Hydration 是浏览器接管服务端或构建阶段已经生成的 HTML,并恢复组件状态、事件处理和客户端路由的过程。页面是否需要 Hydration 取决于是否需要客户端交互,而不是取决于它使用 SSR、SSG 还是 ISR。

客户端会根据相同的组件和初始数据计算首次 UI,复用已有 DOM,并建立后续更新所需的运行时结构。因此,SSR 可以改善内容出现时间,但不会自动消除客户端 JavaScript 的下载、解析和执行成本。

Hydration mismatch

服务端输出与客户端首次渲染结果不一致时,会出现 Hydration mismatch。运行时可能修正或重建受影响的节点,造成警告、额外计算、内容闪烁,甚至丢失焦点或用户输入。

常见原因包括:

  • 首次渲染直接读取 windowdocumentlocalStorage 或视口宽度;
  • 渲染过程中使用 Date.now()Math.random() 等非稳定值;
  • 服务端与浏览器的时区或语言环境不同;
  • 两端使用了不同的数据或初始状态;
  • 非法 HTML 嵌套被浏览器自动修正;
  • 第三方脚本或浏览器扩展在 Hydration 前修改了 DOM。

修复的核心是让两端首次渲染使用相同输入,并保持结构和文本一致。依赖浏览器环境的数据可以先使用稳定默认值,等客户端接管后再更新;无法避免的差异应限制在明确的局部边界内。

选择渲染策略

判断依据

可以从是否需要预渲染、是否依赖请求上下文,以及内容如何更新三个方面判断:

条件优先方式
不需要预渲染,页面主要是复杂交互CSR
需要预渲染,且内容依赖身份、权限或实时查询条件SSR
内容可在构建期确定,重新构建成本可以接受SSG
内容可共享缓存,并允许一定更新延迟ISR

常见场景

实际项目可以按路由、页面区域或组件选择不同的渲染策略。常见场景如下:

场景优先方式Hydration
营销落地页SSG按交互决定
文档类页面SSG按交互决定
内容稳定、偶尔更新ISR按交互决定
依赖公开路由参数、结果可共享ISR按交互决定
依赖请求上下文、实时数据SSR按交互决定
静态内容、局部动态区域SSG + CSR按交互区域决定
重交互、不需要 SEOCSR不适用

工程实现

服务端渲染 API

React 和 Vue 都提供底层服务端渲染 API。这些 API 负责把组件树转换为 HTML;路由、数据加载、缓存和资源组装通常由应用或上层框架处理。

React 常见的服务端 API 包括:

  • renderToString:同步返回 HTML 字符串,不支持流式输出,也不会等待异步内容;
  • renderToPipeableStream:面向 Node.js Streams 的流式输出;
  • renderToReadableStream:面向 Web Streams 的流式输出;
  • renderToStaticMarkup:生成不用于 Hydration 的静态 HTML;
  • prerender:等待数据加载完成后生成静态 HTML,输出 Web Stream;
  • prerenderToNodeStream:等待数据加载完成后生成静态 HTML,输出 Node.js Stream。

Vue 通常使用 createSSRApp 创建应用,再通过 vue/server-rendererrenderToString 生成 HTML 字符串。

渲染结果是完整文档还是页面中的一部分,取决于组件树是否包含 <html><head><body> 等文档结构,也取决于应用如何组装响应。页面需要 Hydration 时,还要附上客户端入口,并保证两端首次渲染使用相同的路由、数据和默认状态。

服务端与客户端产物

可交互的服务端渲染应用通常需要两类构建产物:

  • Server bundle 运行在服务端,负责路由匹配、数据获取和生成 HTML;
  • Client bundle 运行在浏览器,负责 Hydration、客户端导航和状态更新。

两端可以复用组件和业务逻辑,但执行环境不同。构建过程还要生成资源清单,让服务端知道每个路由需要加载哪些 JavaScript 和 CSS。完全不需要交互的页面可以只返回静态 HTML,不加载对应的客户端 bundle。

请求级隔离

服务端进程会连续处理不同用户的请求,模块级变量却可能长期存在。用户相关的可变状态如果放在模块顶层,可能造成跨请求污染。

SSR 应为每个请求创建独立的应用上下文,并绑定路由、用户身份、权限和页面状态。跨请求缓存可以共享,但缓存键必须覆盖所有影响结果的输入,不能让私有数据进入公共缓存。

数据传输与状态一致性

服务端生成 HTML 后,客户端通常还需要同一份数据完成 Hydration。常见做法是把 Hydration 和后续交互所需的初始数据安全地序列化到响应中,供客户端启动时复用。

嵌入 <script> 的内容需要安全转义,同时要保证序列化前后的类型和默认值一致,避免重复请求和首次渲染差异。

流式渲染

传统 SSR 往往要等整棵组件树完成后再返回 HTML。在支持异步边界的框架中,流式渲染可以先返回页面骨架和已经完成的区域,数据获取完成后再发送剩余内容;如果框架支持选择性 Hydration,客户端还可以按边界恢复交互。

流式渲染改变的是内容传输和展示顺序,不会改变 SSR 的基本定义,也不会自动解决缓存和客户端 JavaScript 成本问题。

缓存与失效

设计缓存时至少要回答以下问题:

  • 哪些输入会影响 HTML;
  • 页面能否在不同用户之间共享;
  • 缓存位于浏览器、CDN、反向代理还是应用进程;
  • 内容更新后如何失效;
  • 数据获取或重新生成失败时能否返回旧内容。

包含用户身份、权限或个性化数据的页面不能直接使用公共缓存。可以先生成公共静态内容,再通过接口请求或受正确缓存键约束的私有缓存获取用户数据。

总结

CSR、SSR 和 SSG 的核心区别是页面分别在浏览器运行时、请求期间和构建阶段生成。ISR 为可共享的静态内容增加失效与重新生成机制;Hydration 与生成时机无关,负责让预渲染页面在浏览器中恢复交互。最终采用哪种策略,取决于页面是否需要预渲染、是否依赖请求上下文,以及内容的更新方式。