陌上人如玉
公子世无双

9.1 CSR / SSR / SSG / ISR

9.1 CSR / SSR / SSG / ISR

一句话:四种渲染方式的本质区别是「HTML 在哪里、什么时候生成」——CSR 在浏览器(首屏慢、SEO 差),SSR 每次请求在服务端,SSG 构建时生成(最快),ISR 静态生成 + 后台增量再生。

一、四种方式的原理

① CSR(Client-Side Rendering)
   服务端只返回「空的 HTML + JS」→ 浏览器下载并执行 JS → React 渲染出内容
   代表:CRA / Vite SPA

② SSR(Server-Side Rendering)
   每次请求时,服务端执行 React 把「完整 HTML」生成好 → 返回给浏览器
   → 浏览器立刻显示 HTML(可见)→ 下载 JS → 水合(绑定事件、变成可交互)
   代表:Next.js 的 server rendering / Remix

③ SSG(Static Site Generation)
   构建时(build)就把 HTML 生成好,部署到 CDN
   → 请求时直接返回静态文件(最快、最省)
   代表:Next.js 的静态导出 / Astro

④ ISR(Incremental Static Regeneration)
   静态生成 + 「按需/定时重新生成」——先给用户旧的静态页,
   后台在「过期后」重新生成(下次请求得到新版本)
   代表:Next.js 的 `revalidate`
一句话记忆:
   CSR:HTML 在【浏览器】生成(运行时)
   SSR:HTML 在【服务端】生成(每次请求)
   SSG:HTML 在【构建时】生成(一次性)
   ISR:SSG + 【定期/按需重新生成】

二、对比

维度 CSR SSR SSG ISR
HTML 生成时机 浏览器运行时 每次请求 构建时 构建时 + 后台再生
首屏内容可见 ❌(白屏,等 JS) ✅(立刻看到 HTML) ✅(最快) ✅
可交互时间(TTI) 早(已下载 JS) 较晚(要水合) 最早 最早
SEO ⚠️ 差(爬虫可能看不到内容) ✅ 好 ✅ 好 ✅ 好
数据新鲜度 实时 实时 构建时(可能很旧) 可配置(秒~天)
服务端成本 低(只有静态文件) 高(每请求都渲染) 极低(CDN) 低
首次访问延迟 低(静态文件) 高(等渲染) 极低 极低
适合 后台系统、登录后应用 个性化/实时内容 博客、文档、营销页 电商列表、内容站
TTFB 低 高 最低 最低
⭐ 三个容易混淆的指标:
· FCP/LCP(首次内容/最大内容绘制)——「用户多快看到内容」
· TTI(可交互时间)——「用户多快能操作」
· TTFB(首字节时间)——「服务端多快开始返回」

SSR 的 FCP 很好(HTML 直接可见),但 TTI 未必好(要等 JS 下载 + 水合)。
这一点常被误解为「SSR 一定更快」——准确说法是「SSR 让内容更早可见」。

三、CSR 的两个问题

// 典型 CSR 的 index.html
<!DOCTYPE html>
<html>
  <head><title>App</title></head>
  <body>
    <div id="root"></div>              {/* ⭐ 空的!内容靠 JS 生成 */}
    <script src="/assets/index-4f3a.js"></script>
  </body>
</html>
问题一:首屏「白屏」时间长
   下载 JS → 解析 → 执行 → React 渲染 → 请求数据 → 再渲染
   → 用户在看到内容前要等「网络 + 解析 + 执行 + 请求」的总和

问题二:SEO 差
   · 传统爬虫(不执行 JS)只会看到空 HTML
   · 即使现代搜索引擎能执行 JS,也存在「抓取预算」「渲染超时」的问题
   · 社交平台(微信/Twitter/Facebook)的「分享卡片」只读 HTML meta,
     如果 meta 是 JS 动态插入的,卡片就抓不到(除非用 SSR/预渲染)

问题三:首屏依赖网络质量(弱网体验差)
⚠️ 什么时候 CSR 是可以接受的?
① 登录后的后台系统(不需要 SEO、用户有耐心等)
② 强交互应用(编辑器、IDE 类)
③ 内部工具
→ 但即使是这些,「首屏白屏 + 骨架」的体验优化仍然值得做(代码分割、预加载)

四、SSR 的完整流程

一次 SSR 请求经历:

服务端:
  ① 接收请求
  ② 渲染 React 树 → 生成 HTML 字符串(或流式输出)
  ③ 同时「脱水」把「渲染时用到的数据」序列化到 HTML 里
     (如 <script>window.__DATA__ = {...}</script>)
  ④ 返回 HTML

浏览器:
  ⑤ 立刻显示 HTML(用户看到内容 —— FCP 好)
  ⑥ 下载并执行 JS(React 客户端代码)
  ⑦ 【水合 Hydration】:
     · 复用服务端的 HTML,绑定事件监听
     · 校验「客户端渲染结果」与「服务端 HTML」是否一致
     · 不一致 → hydration mismatch(警告并可能重新渲染)
  ⑧ 页面变为「可交互」(TTI)

⚠️ 关键:水合之前,页面是「看得见但点不动」的
   —— 这是 SSR 的固有特性(也是「选择性水合」「岛屿架构」要解决的问题)
// Next.js(App Router)里的 SSR
// app/page.tsx —— Server Component 默认是 SSR
export default async function Page() {
  const posts = await db.post.findMany();      // ⭐ 服务端直接查数据库
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

五、SSG 与 ISR

// SSG:构建时生成(Next.js 里「没有动态数据」的页面默认静态化)
// app/about/page.tsx
export default function About() {
  return <div>关于我们</div>;                   // ⭐ 构建时生成 HTML
}
// ISR:静态生成 + 定期再生(Next.js 的 revalidate)
// app/posts/page.tsx
export const revalidate = 60;                   // ⭐ 60 秒后「过期」

export default async function Posts() {
  const posts = await fetchPosts();
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
ISR 的工作方式(Next.js):
① 构建时生成静态页
② 用户访问 → 直接返回静态页(很快)
③ 页面「超过 revalidate 时间」后,第一个访问它的用户:
   · 仍然拿到「旧的静态页」(不等渲染)
   · 同时后台触发「重新生成」
④ 之后访问的用户拿到新版本

⭐ 这就是 stale-while-revalidate(与 7.5 的思想一致):
   先给用户旧内容(快),后台再更新(新)。
// ✅ 按需再生(On-demand Revalidation):内容变更时主动刷新
import { revalidatePath, revalidateTag } from 'next/cache';

export async function createPost(formData: FormData) {
  'use server';
  await db.post.create({ data: { title: formData.get('title') as string } });
  revalidatePath('/posts');                    // ⭐ 让 /posts 的缓存立刻失效
  // 或 revalidateTag('posts');
}
// 比「等 revalidate 时间」更实时
方式 数据新鲜度 说明
SSG(无 revalidate) 构建时 内容变了要重新部署
ISR(时间) revalidate 秒 页面级自动刷新
ISR(按需) 立刻 内容变更时调 revalidatePath/revalidateTag
SSR 每次请求 最实时,但成本最高

六、流式 SSR 与 RSC

// ✅ 流式 SSR(React 18+):shell 先到,慢的部分后补
// 用 renderToPipeableStream + Suspense 边界
<Suspense fallback={<CommentsSkeleton />}>
  <Comments />                                  {/* ⭐ 服务端先给占位,数据就绪后流式补上 */}
</Suspense>
// ✅ React Server Components(RSC):组件在服务端执行,代码不进客户端包
// app/posts/page.tsx(Server Component,默认)
import { db } from '@/lib/db';
import { LikeButton } from './like-button';     // Client Component

export default async function PostsPage() {
  const posts = await db.post.findMany();       // ⭐ 直接访问数据库(这段代码在服务端)
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>
          {p.title}
          <LikeButton postId={p.id} />          {/* 客户端组件负责交互 */}
        </li>
      ))}
    </ul>
  );
}
流式 SSR + RSC 的收益:
① shell 立刻可见(TTFB 早),慢的部分流式补上
② 服务端组件的代码「不进客户端包」→ 包体积显著减小
③ 可以在服务端「直接访问数据库」→ 少一层 HTTP 往返
④ 配合 Suspense 可以做「渐进式渲染 + 选择性水合」

⚠️ 代价与限制:
① 心智模型变复杂(Server/Client 边界,见 9.3)
② 依赖框架(Next.js / Remix 等)
③ 调试与部署更复杂(服务端运行环境、缓存策略)

七、选型决策

① 需要 SEO 吗?
   ├─ 不需要(后台系统、内部工具)→ CSR 就够了(可以用 Vite)
   └─ 需要 → 继续

② 内容「所有人都一样」且「变化不频繁」吗?(博客、文档、营销页)
   ├─ 是 → SSG(配 ISR 处理内容更新)——最快、最省
   └─ 否 → 继续

③ 内容「因人而异」或「必须实时」吗?(用户中心、实时数据)
   ├─ 是 → SSR(或「SSR + 客户端数据获取」混合)
   └─ 否 → ISR(静态 + 定期更新,性价比最高)

④ 需要「首屏极快 + 强交互」吗?(电商、内容平台)
   → SSR/RSC + 流式渲染 + 客户端数据获取(混合策略)

⭐ 实践中最好是「混合」:
   首页/列表 → ISR(快 + 可更新)
   详情页 → SSR 或 ISR
   登录后页面 → CSR(或 SSR 拿首屏 + 客户端交互)
   静态页(关于/条款)→ SSG

八、常见坑

// ① 认为「SSR 一定比 CSR 快」
// → SSR 的 FCP 好,但 TTI 可能更差(要等 JS + 水合)
// ② 认为「用了 SSR 就不需要客户端数据获取」
// → 首屏数据用 SSR 更好,但「交互产生的数据」(筛选/分页)仍需客户端请求
// ③ 在 SSR 组件里用「浏览器 API」
window.localStorage.getItem('x');            // ❌ 服务端没有 window
// ✅ 用 cookie(通过 request headers)或在客户端 useEffect 里读
// ④ 在 SSR 组件里用「随机值 / 当前时间」渲染内容
const id = Math.random();                     // ⚠️ hydration mismatch
// ⑤ 认为「SSG 内容永不过期」(没有 revalidate 时,内容变了要重新部署)
// ⑥ 用 SSR 渲染「完全静态的页面」(浪费服务端资源,应该用 SSG)
// ⑦ 忘记「SSR 的数据要脱水给客户端」(否则客户端再请求一次,甚至 mismatch)
// ⑧ 用「ISR + 个性化内容」(ISR 是共享缓存,会串用户数据!)
// ⑨ 在 SSR 中「把敏感数据渲染进 HTML」(任何人都能看到源码)
const token = req.cookies.token;              // ⚠️ 别渲染到 HTML 里
// ⑩ 忽略「服务端渲染的错误处理」(一个组件抛错可能让整个请求 500)
// ⑪ 认为「RSC 取代了 SSR」(RSC 是「组件在哪执行」,SSR 是「HTML 何时生成」)
// ⑫ 在「边缘运行时」用了 Node 专属 API(如 fs)
// ⑬ 用 SSR 但不做「缓存」(每个请求都查数据库,成本高、更慢)
// ⑭ 忘记水合成本(大量静态 HTML + 大量 JS → 水合很慢)
//    → 用「岛屿架构」(Astro)或「RSC + 少的客户端组件」缓解
// ⑧ 的说明:ISR 是「共享缓存」,不能用来渲染「因人而异」的内容
// ❌ 页面里渲染了 userId 相关内容,但页面是 ISR 静态化的
// ✅ 个性化部分用「客户端获取」或用「动态渲染(SSR)」

九、面试延伸

  1. 「CSR / SSR / SSG / ISR 有什么区别?」
    本质区别是「HTML 在哪里、什么时候生成」:① CSR——服务端只返回空 HTML + JS,内容由浏览器执行 JS 渲染(首屏白屏、SEO 差,但服务端成本低);② SSR——每次请求时服务端生成完整 HTML(首屏内容可见、SEO 好、数据实时,但服务端成本高、TTFB 慢);③ SSG——构建时生成 HTML 并部署到 CDN(最快最省,但内容可能很旧);④ ISR——静态生成 + 后台定期/按需再生(既有静态的「快」,又能更新内容)。

  2. 「SSR 一定比 CSR 快吗?」
    不一定。准确说法是「SSR 让内容更早可见(FCP/LCP 更好)」,但它的 TTI(可交互时间)可能更差——因为浏览器仍需「下载 JS → 水合(绑定事件)」,而水合期间页面是「看得见但点不动」的。如果 JS 很大、水合很慢,用户体验反而可能是「刚看到内容就想点,但点了没反应」。所以优化的目标应该是「用更少的 JS 完成水合」(代码分割、减少客户端组件、岛屿架构)。

  3. 「SSR 的完整流程是什么?」
    服务端:① 接收请求 → ② 渲染 React 树为 HTML → ③ 把渲染用到的数据「脱水」序列化进 HTML(如 window.__DATA__)→ ④ 返回 HTML。浏览器:⑤ 立刻显示 HTML(内容可见)→ ⑥ 下载并执行客户端 JS → ⑦ 水合(复用 HTML、绑定事件、校验一致性)→ ⑧ 变为可交互。关键点是第 ⑦ 步的「水合」——它是 SSR 的核心机制,也是「hydration mismatch」问题(见 9.4)的来源。

  4. 「什么是 ISR?」
    ISR(Incremental Static Regeneration)是「静态生成 + 后台再生」:构建时生成静态页;页面超过 revalidate 时间后,第一个访问它的用户仍然拿到旧的静态页(不等渲染),同时后台触发重新生成,之后的用户拿到新版本。这本质上是「页面级的 stale-while-revalidate」。此外还支持「按需再生」——内容变更时调用 revalidatePath / revalidateTag 立刻让缓存失效(比等时间更实时)。

  5. 「什么时候用 CSR?」
    三种场景:① 登录后的后台系统 / 内部工具——不需要 SEO,用户愿意等(且首屏可以通过代码分割优化);② 强交互应用——编辑器、IDE 类工具(客户端的计算与状态占主导,服务端渲染收益小);③ 数据高度个性化且实时——反正要客户端请求,SSR 的意义有限(但首屏骨架仍可优化)。判断标准是「SEO 与首屏可见性是否关键」——如果不关键,CSR 的开发与部署成本最低。

  6. 「SSG 和 ISR 怎么选?」
    看「内容更新频率与实时性要求」:① 几乎不变(关于页、条款、文档)→ SSG(无 revalidate);② 定期更新(博客列表、商品列表)→ ISR + 时间(如 revalidate = 60);③ 内容变更时要立刻生效(CMS 发布)→ ISR + 按需再生(revalidatePath);④ 必须实时且因人而异(用户中心)→ SSR 或「SSG 外壳 + 客户端数据」。关键坑:ISR 是共享缓存,不能渲染个性化内容(否则会把 A 用户的数据给 B 用户看)。

  7. 「什么是流式 SSR?它解决什么问题?」
    流式 SSR 指「服务端边渲染边发送 HTML」:先把「不挂起的部分(shell)」渲染完立即发送(TTFB 更早、首屏更快可见),遇到 <Suspense> 边界里「慢的部分」时先发送fallback 的 HTML + 一段脚本,数据就绪后再把真正内容流式发送并由客户端无缝替换。它解决的是「一个慢请求阻塞整个页面」的问题(传统 SSR 必须等所有数据就绪才能返回 HTML)。实现上要用 renderToPipeableStream(React 18+)。

  8. 「RSC(Server Components)和 SSR 有什么区别?」
    它们回答的是不同的问题:① SSR 回答「HTML 什么时候生成」(每次请求时、在服务端)——它仍然会「渲染完整组件树、把 JS 发给客户端、在客户端水合」;② RSC 回答「组件在哪里执行」——服务端组件在服务端执行,其代码不进客户端包(可以直接访问数据库、用大依赖而不增加客户端体积),只有被标记 "use client" 的组件才会进客户端包并参与水合。两者可以同时使用(如 Next.js App Router:用 RSC 减少客户端 JS,同时用 SSR/ISR 决定 HTML 生成方式)。

一句话速记

四种方式的本质区别是「HTML 在哪里、什么时候生成」——CSR(浏览器运行时,首屏白屏 + SEO 差但成本最低)、SSR(每次请求,内容实时 + SEO 好但 TTFB 慢、TTI 未必好)、SSG(构建时,最快最省但可能很旧)、ISR(静态 + 后台/按需再生,本质是「页面级 stale-while-revalidate」);SSR 的流程是「服务端渲染 HTML + 数据脱水 → 浏览器显示 → 水合 → 可交互」(水合期间看得见但点不动);选型口诀是「不要 SEO 用 CSR;不变的内容用 SSG;定期更新用 ISR;实时/个性化用 SSR」,实践上多为混合;ISR 不能渲染个性化内容(共享缓存会串用户);RSC 解决的是「组件在哪执行」(不进客户端包),与 SSR 是两件事,可以同时用。

赞(0) 打赏
未经允许不得转载:陌上寒 » 9.1 CSR / SSR / SSG / ISR

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫