13 · 渲染模式:CSR / SSR / SSG / ISR / RSC
本篇解决什么问题: "要不要上 SSR" 是个架构决策,代价是服务端成本 + 复杂度 + 水合开销。 本篇给一张判断表,让你按首屏要求 / 内容更新频率 / 个性化程度直接选,而不是跟风。
📌 关键认知:没有最优模式,只有最匹配业务的模式;而且同一站点不同页面可以用不同模式(Next.js 的核心卖点)。
开篇速览
| 模式 | HTML 在哪生成 | 首屏 | SEO | 服务端成本 | 适合 |
|---|---|---|---|---|---|
| CSR | 浏览器 | 慢(白屏 → 骨架 → 内容) | ❌ 差 | 无(纯静态托管) | 后台系统、重交互应用 |
| SSR | 每次请求时服务端 | 快 | ✅ 好 | 高(每次都算) | 内容站、电商详情、需 SEO + 实时 |
| SSG | 构建时 | 最快 | ✅ 好 | 无 | 文档、博客、营销页 |
| ISR | 构建时 + 按需增量再生成 | 快 | ✅ 好 | 低 | 大量内容页(商品、文章) |
| RSC | 服务端组件在服务端渲染,客户端组件在浏览器 | 快 | ✅ 好 | 中 | React 生态的精细化拆分 |
一、五种模式逐个拆
1.1 CSR(Client-Side Rendering)
浏览器 ──► 空 HTML(只有一个 <div id="root">)
──► 下载 JS bundle
──► 执行 JS,渲染出 DOM
──► 发请求拿数据
──► 渲染内容优点:部署极简(静态文件扔 CDN)、交互体验好、前后端彻底分离。 缺点:首屏白屏、SEO 差、性能依赖用户设备。
💡 Flutter Web 天然只能 CSR:它编译出的就是一个巨大的 JS bundle + canvas 渲染,没有服务端 HTML 生成这一环。
1.2 SSR(Server-Side Rendering)
浏览器 ──► 请求
服务端 ──► 执行组件、拉数据、拼出完整 HTML
──► 返回 HTML(首屏立即可见)
浏览器 ──► 下载 JS → **水合(Hydration)** → 可交互水合 = 把服务端渲染的静态 DOM "接上" React/Vue 的事件与状态,让它变成可交互的应用。
代价(必须知道):
| 代价 | 说明 |
|---|---|
| 服务器成本 | 每次请求都要执行组件 + 拉数据 |
| TTFB 变慢 | 服务端要先算完才能返回 |
| 水合阻塞 | HTML 下来了但不能点,要等 JS 下载并执行完 |
| 运维复杂度 | Node 服务要监控、扩容、容灾(见 A23 · Node.js 与 BFF) |
1.3 SSG(Static Site Generation)
构建时就把所有页面生成成静态 HTML,运行时直接由 CDN 返回。
优点:最快、最安全、成本最低。 缺点:内容变更必须重新构建;页面数多时构建时间线性增长。
📌 本站(sunqy-doc)就是 SSG——VitePress 在构建时生成全部静态页,部署到 Cloudflare Pages。
1.4 ISR(Incremental Static Regeneration)
SSG 的增强:静态页面生成后,按过期时间(或按需)在后台重新生成。
// Next.js
export async function getStaticProps() {
return { props: { data }, revalidate: 60 } // 60 秒内用缓存,过期后后台再生成
}优点:兼顾 SSG 的速度与内容的相对新鲜;页面再多也不用全量构建。 代价:可能有短暂旧内容(stale-while-revalidate);需要支持 ISR 的平台。
1.5 RSC(React Server Components)
不是一个"页面级"模式,而是组件级的拆分:
| 组件类型 | 在哪执行 | 能做什么 | 不能做什么 |
|---|---|---|---|
| Server Component | 只在服务端 | 直接读数据库/文件系统、代码不进 bundle | 不能用 useState/useEffect/浏览器 API |
| Client Component | 服务端预渲染 + 浏览器水合 | 交互、状态、事件 | 不能直接访问后端资源 |
核心价值:大幅减少客户端 bundle——服务端组件的依赖(比如一个巨大的 markdown 解析器)不会被下载。
二、选型判断框架(本篇的重点)
按三个问题依次回答:
问题 1:需要 SEO / 首屏速度吗?
- ❌ 不需要(后台系统、内部工具、重交互编辑器)→ CSR,别折腾
- ✅ 需要 → 进入问题 2
问题 2:内容是"相对固定"还是"实时 / 个性化"?
- 相对固定(文档、博客、营销页)→ SSG(最快最省)
- 实时或个性化(价格、库存、登录后内容)→ SSR
- 内容多且会更新(商品详情、新闻)→ ISR(SSG 的速度 + 可更新)
问题 3:是否已用 React 且 bundle 体积是瓶颈?
- 是 → 考虑 RSC(把静态展示部分下沉到服务端组件)
- 否 → 不必为了 RSC 上 RSC
决策速查表
| 场景 | 推荐 |
|---|---|
| 后台管理系统 | CSR |
| 公司官网 / 营销页 | SSG |
| 技术文档 / 博客 | SSG(本站即如此) |
| 电商商品详情(量大、价格会变) | ISR |
| 电商首页(强个性化 + SEO) | SSR |
| 社交 Feed(登录态、实时) | CSR + SSR 首屏 混合 |
| React 应用 bundle 过大 | RSC 拆分 |
三、水合(Hydration)的代价:SSR 不是免费的
这是最容易被忽略的代价,也是面试高频追问点。
时间轴:
① HTML 返回 → 用户**看到**内容(FCP 好)
② JS 下载 → 还不能交互
③ JS 执行 + 水合 → 终于能点(TTI)问题:① 和 ③ 之间有一段 "看得见但点不动" 的恐怖谷。 页面越复杂、bundle 越大,这段越长——用户会疯狂点击但毫无反应。
三种缓解方案:
| 方案 | 思路 |
|---|---|
| 流式 SSR(Streaming) | HTML 分块返回,配合 Suspense,先出骨架先水合 |
| 选择性水合(Selective Hydration) | 按交互优先级分批水合,谁先被点谁先水合(React 18+) |
| 岛屿架构(Islands) | 只有交互"岛屿"需要 JS,其余全是静态 HTML(Astro 的核心) |
✅ 理解"恐怖谷"是区分背概念与真懂的分水岭。
四、SSR 落地要额外处理的事
上 SSR 前,这些问题必须有答案:
| 问题 | 说明 |
|---|---|
服务端没有 window | 组件里访问 window/document/localStorage 会崩 → 必须放在 useEffect 或做 typeof window !== 'undefined' 判断 |
| 数据请求重复 | 服务端拉了一次,水合时又拉一次 → 需要把服务端数据序列化注入到 HTML(如 Next.js 的 __NEXT_DATA__) |
| 样式闪动(FOUC) | 服务端要能提取并内联关键 CSS |
| 缓存与扩容 | SSR 是 CPU 密集,需要缓存 HTML 片段 + 弹性扩容 |
| 错误边界 | 服务端渲染抛错会导致整页 500,必须有兜底 |
自检清单
- [ ] 能说出 5 种模式各自的 HTML 生成时机与适用场景
- [ ] 能解释水合是什么,以及它带来的"看得见但点不动"问题
- [ ] 能按"SEO 需求 → 内容更新频率 → bundle 瓶颈"三步给出选型
- [ ] 能说出 SSR 落地的 3 个以上额外成本(服务端无
window、数据重复请求、FOUC 等) - [ ] 能解释为什么 Flutter Web 只能 CSR
常见误区
| ❌ 误区 | ✅ 事实 |
|---|---|
| "SSR 一定比 CSR 快" | SSR 的 TTFB 更慢、TTI 可能更慢;赢的是 FCP 和 SEO |
| "SSG 的页面不能更新" | 重新构建即可;量大时用 ISR |
| "RSC 是 SSR 的替代品" | RSC 是组件级拆分,可与 SSG/ISR 组合 |
| "上了 SSR 就不用管 bundle 体积" | 恰恰相反:bundle 越大,水合越慢,恐怖谷越长 |
动手练习
- 用 VitePress 新建一个页面,
npm run docs:build后到dist里确认它是纯静态 HTML(体会 SSG)。 - 找你现在项目的一个页面,按"三问框架"判断它应该用哪种模式,并写下理由。
- 在 Chrome 里开 CPU 4× 降速,观察一个 SSR 站点的"看得见但点不动"窗口有多长。
相关文档
- 首屏与加载性能治理 → A10 · Web 性能优化
- 性能指标怎么度量(LCP/INP/CLS)→ A25 · 前端性能度量与诊断
- SSR 的服务端落地 → A23 · Node.js 与 BFF
- 跨端方案里的 Flutter Web → A01 · 跨端方案横向对比