CSR / SSR / SSG介绍

2 阅读13分钟

CSR / SSR / SSG:渲染模式、Hydration 与 Streaming SSR(以 Next.js / Nuxt 为例)

本文用尽量统一的术语解释三种常见渲染模式(CSR、SSR、SSG),并补充传统 SSR 的 Hydration/同构注意事项,以及 Streaming SSR(React 18 + Suspense)解决的性能瓶颈。内容偏 Next.js / Nuxt 的工程实践视角,但结论适用于大多数现代前端框架。


1. 基本概念

  • CSR(客户端渲染):HTML 基本是“壳”,主要靠浏览器下载并执行 JS,再在客户端生成页面内容。
  • SSR(服务端渲染):请求到达服务端后,服务端把页面渲染成 HTML 返回;浏览器先展示内容,再下载 JS 进行 Hydration(注水/水合) 来接管交互。
  • SSG(静态生成):在构建阶段就把页面生成成静态 HTML(以及必要的静态资源),部署到 CDN;用户请求时直接返回已生成内容。

它们不是互斥关系:在 Next.js/Nuxt 里,通常是按路由/页面粒度混用,同一个站点里既可能有 SSG 页面,也可能有 SSR 页面,还可能局部 CSR。


2. 如何理解“服务器压力”和“请求时机”

2.1 CSR:把“渲染成本”放在客户端

CSR 的典型特征:

  • 首个 HTML 很轻(通常只有一个 root 容器)
  • 首屏内容依赖 JS 执行(下载、解析、执行都需要时间)
  • 用户在页面内的交互(不涉及请求的部分)不需要服务端参与
  • 服务端主要提供:静态资源(JS/CSS/图片)+ API

对服务器而言,压力往往集中在:

  • 静态资源分发(可用 CDN 缓解)
  • API 请求(与业务交互强相关)

对用户体验而言,CSR 更容易出现:

  • 首屏慢:JS 包大、执行重、弱网/弱机更明显
  • SEO 不友好:纯 CSR 需要额外方案(预渲染、SSR、动态渲染等)

2.2 SSR:把“首屏渲染成本”放在服务端

SSR 的典型特征:

  • 请求到达服务端时,服务端会在 Node/Edge 等运行时执行页面逻辑、拉取数据、生成 HTML
  • 浏览器拿到 HTML 后可以更快看到内容(通常 FCP/LCP 更好)
  • 之后仍需要下载 JS 来 Hydration,完成可交互

需要澄清的一点:在 Next.js/Nuxt 这类框架里,是否“每次页面切换都重新请求整页 HTML”取决于架构

  • 传统多页(MPA):点击链接会发起整页导航,通常会请求新的 HTML 文档。
  • SSR + 客户端路由(常见于 Next/Nuxt):首屏可能 SSR 出 HTML,但后续路由切换往往是 SPA 体验,可能只请求数据/路由资源,而不是完整 HTML 文档。

服务器压力主要来自:

  • SSR 期间的渲染计算(组件执行、模板生成)
  • SSR 期间的数据请求(接口慢会拖慢 TTFB(首字节输出)/整体响应)
  • 并发下的 CPU/内存消耗(需要缓存、降级、限流等体系)

2.3 SSG:把“渲染成本”前置到构建阶段

SSG 的典型特征:

  • 页面在 build 时就生成好(HTML + 静态资源)
  • 运行时只做静态分发,可直接上 CDN,请求延迟极低
  • 缺点是:页面内容不是实时的,更新需要重新构建/重新部署(或使用增量更新能力)

SSG 特别适合:

  • 内容相对稳定的站点(博客、文档、营销页、帮助中心)
  • 对首屏性能和成本敏感的页面

3. 关键指标:TTFB / FCP / LCP 与“体感速度”

常见指标含义(简化版):

  • TTFB(Time To First Byte):浏览器收到首字节的时间,反映服务端响应快慢与网络条件。
  • FCP(First Contentful Paint):首次渲染出“有内容”的时间。
  • LCP(Largest Contentful Paint):最大内容元素完成渲染的时间,常用于衡量首屏主要内容出现的速度。

大致对比(不考虑实现细节时):

  • CSR:TTFB 往往不错(HTML 很快),但 FCP/LCP 可能受 JS 包体/执行影响更明显
  • SSR:TTFB 可能更高(服务端要渲染/取数),但 FCP/LCP 往往更好
  • SSG:TTFB/FCP/LCP 通常都很好(CDN + 静态 HTML)

4. 传统 SSR 的两阶段:HTML 先到、交互后到

传统 SSR 可以拆成两步理解:

  1. 服务端阶段
    • 执行组件逻辑与数据获取
    • 生成完整 HTML(通常还会把“首屏所需的数据”序列化到页面中)
    • 返回给浏览器
  2. 浏览器阶段(Hydration)
    • 浏览器展示 HTML(此时内容可见,但交互可能尚未可用或不完整)
    • 下载 JS bundle
    • React/Vue 在客户端运行一次组件逻辑,生成虚拟树并与现有 DOM 对齐,然后挂载事件监听器,使页面变得可交互

这一步“把静态 HTML 变成交互应用”的过程,就叫:

  • React:Hydration(注水/水合),常见入口为 hydrateRoot(...)
  • Vue/Nuxt 也有对应的 hydration 过程(概念一致)

5. Hydration 的常见坑:同构与 Hydration Mismatch

SSR 场景常说“同构(isomorphic/universal)”,意思是:

同一套组件/页面代码,会在服务端执行一次、在客户端再执行一次。

因此需要格外注意“环境差异”:

  • 服务端没有 window / document / localStorage
  • 服务端与客户端的时间、随机数、地区/时区、语言等可能不同
  • 某些副作用只应发生在客户端(例如埋点、读取视口、访问存储)

5.1 Hydration mismatch 常见原因

Hydration mismatch 指:客户端算出来的 UI 结构与服务端 HTML 不一致,框架可能会警告或直接替换节点(造成闪烁、性能回退)。

高频原因包括:

  • Math.random() / crypto 随机值
  • new Date()(时区/时间差)
  • 依赖运行环境差异的分支渲染(例如用 typeof window !== 'undefined' 改变了结构)
  • SSR 与 CSR 初始数据不一致(序列化/反序列化丢失,或请求时机不同)

5.2 建议写法(概念示例)

  • 把只在客户端可做的事情放到客户端生命周期里(React 的 useEffect、Vue 的 onMounted),并确保它不影响首屏结构。
  • 把首屏需要的稳定数据放到服务端并序列化给客户端,保证两边“首次渲染输入一致”。
  • 对必须 client-only 的 UI 做隔离(例如只在客户端渲染某一小块,而不是让整页结构变化)。

6. 为什么需要 Streaming SSR:传统 SSR 的“最慢接口拖垮整页”

传统 SSR 的一个核心痛点是:

如果首屏渲染依赖多个数据源,只要其中一个慢,服务端就往往要等到全部数据齐备才能返回完整 HTML。

这会带来:

  • TTFB 变高(用户迟迟收不到首字节)
  • 首屏“白屏更久”,体感变差
  • 页面越复杂,服务端渲染时间越不可控

7. Streaming SSR(React 18 + Suspense)的工作方式

Streaming SSR 的思路是:

先让用户尽快拿到“可展示的骨架/部分内容”,慢的模块晚点再补齐。

核心机制依赖:

  • 流式传输(streaming):服务端不必一次性吐出完整 HTML,可以分段输出。
  • Suspense 边界:遇到“需要等待数据”的子树时,先输出 fallback(占位 UI),等数据好了再把该部分内容流式补上。

7.1 解决的 3 个关键问题(你原文的规范化版本)

  1. 显著降低 TTFB
  • 传统 SSR:等所有数据都 ready 才开始返回
  • Streaming SSR:先返回骨架/已 ready 的部分
  1. 避免“最慢接口拖垮整个页面”
  • 慢模块包在 Suspense 里,只影响自己的区域
  • 页面其他部分可以先渲染并返回给用户
  1. 显著提升用户感知速度
  • 内容逐步出现(progressive reveal)
  • 骨架屏/占位自然过渡

8. 优化方向(把你原文拆成“资源层面 / 架构层面”)

8.1 资源层面(面向“下载与执行成本”)

  • 减少首屏 JS 体积:Tree Shaking(移出未使用的代码)、按路由分包、依赖瘦身(只引入依赖库中的部分模块)
  • 懒加载与预加载:路由级 lazy、组件级 lazy、按需预取关键资源
  • 缓存:HTTP 缓存(Cache-Control/ETag)、CDN 缓存、静态资源长缓存 + hash
  • 图片与字体优化:响应式图片、压缩、合适格式(AVIF/WebP)、字体子集化 字体子集化:原字体集包含很多的字体,但你只用了其中的一小部分,尤其是中文字体,且多运用在SSG,因为页面内容在构建时就已经确定(字体领域的tree shaking)。

8.2 架构层面(面向“渲染策略与感知速度”)

  • 骨架屏 / 占位 UI:让首屏更“有反馈”
  • 数据预取:路由切换前预取数据/代码,降低切换延迟
  • Service Worker(更详细):把它理解为“浏览器里的网络代理层”,最核心是 拦截请求(fetch)+ 可编程缓存(Cache Storage)
    • 能做什么:离线可用、静态资源预缓存、接口请求缓存/兜底、弱网降级、统一缓存策略、后台同步(Background Sync)、推送(Push)。
    • 常见缓存策略:Cache First、Network First、Stale-While-Revalidate(先用缓存快速出内容,再后台更新)。
    • 注意点:需要 HTTPS(安全上下文);作用域受注册路径限制;更新是“新旧版本并存→激活切换”的模型;缓存失效与版本管理要设计好(否则容易“缓存污染/发版不生效”)。
  • Streaming SSR + Suspense:拆分慢模块,避免拖垮首屏

8.3 Service Worker vs Web Worker vs Shared Worker(对比与选型)

你提到的 “shardworker” 通常指的是 Shared Worker。这三者虽然都跑在“主线程之外”,但定位完全不同:

8.3.1 先给结论(最常用的区分维度)
  • Service Worker:偏“网络与缓存层”的能力,核心是 拦截请求(fetch)+ Cache Storage,用于离线、预缓存、弱网优化、统一缓存策略、推送等。
  • Web Worker(Dedicated Worker):偏“计算卸载”的能力,核心是 把 CPU 密集型任务搬到后台线程,减少主线程卡顿。
  • Shared Worker:偏“跨标签页共享后台能力”,核心是 同源多个页面共享一个 worker 实例,用于跨 tab 共享连接/状态/任务队列等。
8.3.2 能力与限制对比
维度Service WorkerWeb Worker(Dedicated)Shared Worker
线程/定位后台事件驱动线程(偏“代理层”)后台线程(偏“计算层”)后台线程(偏“共享层”)
生命周期install/activate + 事件触发;可随时被浏览器回收随页面创建与销毁(被引用时存活)同源多个页面共享;最后一个连接断开后可被回收
是否可拦截网络请求可以fetch 事件)不可以(只能自己 fetch不可以(只能自己 fetch
缓存能力(Cache Storage 常用,配合 fetch 拦截)弱(不负责统一缓存策略)弱(不负责统一缓存策略)
是否可访问 DOM不可以不可以不可以
通信方式与页面通过 postMessage与创建它的页面 postMessage多页面通过 MessagePort 连接同一个 worker
典型用途离线、预缓存、回源降级、请求合并/重试、push、后台同步大 JSON 解析、压缩/解压、图片处理、加密、复杂计算多 tab 复用 WebSocket、共享缓存/状态、统一任务调度
约束需要 HTTPS(安全上下文);作用域受路径限制;调试与更新机制更复杂不能直接提升首屏“HTML 可见速度”,但能减少交互卡顿兼容性/使用习惯相对少;需要设计好多页面协作协议
8.3.3 如何选(按问题类型)
  • 你要解决“弱网/离线/缓存策略/请求兜底”:优先 Service Worker(它是唯一能拦截页面请求的)。
  • 你要解决“页面卡顿/长任务阻塞主线程”:优先 Web Worker(把计算搬走)。
  • 你要解决“多个标签页重复做同一件事(重复连接、重复计算、共享状态)”:考虑 Shared Worker(同源共享实例)。

9. 小结

  • CSR/SSR/SSG 的核心差异在于:页面内容在何时、何地生成(客户端、请求时服务端、构建时)。
  • SSR 的关键是“HTML 先到 + Hydration 交互接管”,同构带来环境差异与 mismatch 风险。
  • Streaming SSR 通过 Suspense 边界把“慢模块”隔离开,显著改善 TTFB 与体感速度。
  • 真实项目通常是混合策略:按页面类型选择 SSG/SSR/CSR,再配合缓存、分包与数据策略做系统优化。

10. Next.js / Nuxt:核心原理与优势(它们解决了什么问题)

这两者本质上都是“基于 React/Vue 的全栈 Web 框架”:在保留组件化与 SPA 体验的同时,把 SSR/SSG/Streaming/缓存/路由/打包/部署 这一整套工程问题做成标准化的渲染管线。

10.1 一条请求的渲染链路(从 URL 到 HTML/交互)

  • 路由与页面模块发现
    • Next.js:以“文件/约定”为中心发现路由与页面(并在构建时生成路由映射与产物清单)。
    • Nuxt:同样用约定式路由,编译期产出路由与页面 bundle 关系。
  • 选择渲染策略(CSR/SSR/SSG/增量/流式)
    • 框架会在“构建期/请求期/客户端”之间分配渲染工作:能静态的尽量静态,必须实时的再走 SSR;页面内慢模块可用 Suspense/异步边界拆开。
  • 服务端渲染输出(HTML 与可水合信息)
    • 服务端返回 HTML(必要时分块流式输出),并携带让客户端能接管的上下文(例如构建清单、路由信息、初始数据/状态的序列化)。
  • 客户端接管(Hydration + 客户端路由)
    • 浏览器先展示 HTML,再加载 JS;Hydration 后获得完整交互,后续路由切换大概率是 SPA 体验(按需加载代码与数据)。

10.2 为什么它们能“解决问题”(对应你文档中的痛点)

  • 解决“CSR 首屏慢/SEO 弱”
    • 通过 SSR/SSG/Streaming SSR,让首屏直接有内容(改善 FCP/LCP 与可爬取性),再由客户端接管交互。
  • 解决“纯 SSR 服务器压力大、最慢接口拖垮整页”
    • 通过 Streaming + 边界拆分(React Suspense / Vue 异步组件与分块),把慢模块隔离;再结合缓存/边缘运行时/静态化,把大量请求从“实时渲染”迁移到“静态分发”。
  • 解决“工程复杂度过高(路由、构建、代码分割、资源优化、部署形态)”
    • 框架把这些能力变成默认路径:自动代码分割、产物清单、静态资源 hash、运行时加载策略、路由与页面边界等。
  • 解决“同构带来的工程坑难以规模化治理”
    • 通过约定与编译期约束,减少服务端/客户端代码混用的概率;并提供明确的边界(哪些模块只能在客户端,哪些可以在服务端/边缘执行)。

10.3 带来的核心优势(不讲用法,只讲收益)

  • 性能更可控:把“静态化 + 分包 + 流式 + 缓存”组合成一条标准渲染管线,降低首屏不确定性。
  • 成本更可控:静态内容走 CDN,动态内容可用缓存/边缘分担,服务端压力更容易被工程化管理。
  • 研发更可控:减少手写脚手架与自研路由/构建的维护成本,团队更容易统一最佳实践。
  • 渐进增强:同一个项目可按页面/模块粒度渐进演进渲染策略(从 CSR 到 SSR/SSG/Streaming),而不是“推倒重来”。