一、核心概念对比

CSR(客户端渲染) SSR(服务端渲染) SSG(静态站点生成) ISR(增量静态再生)
渲染位置 浏览器 服务器 构建时 构建时 + 按需更新
HTML 生成时机 运行时(浏览器) 每次请求时 build 时一次性生成 构建时生成,过期后按需重新生成
首屏速度 慢(需下载 JS 后渲染) 快(直接返回 HTML) 最快(直接返回静态文件) 快(静态文件 + 按需更新)
SEO 差(爬虫可能拿不到内容) 最好
服务器压力 低(静态资源) 高(每次请求都要渲染) 低(CDN 分发) 低(CDN + 按需渲染)
适用场景 后台管理系统、SPA 动态内容多、SEO 要求高 内容不常变化的页面 内容会变化但不要求实时

二、CSR(客户端渲染)

2.1 工作原理

用户请求 → 服务器返回空 HTML + JS → 浏览器下载 JS JS 执行渲染页面
<!-- 服务器返回的 HTML(几乎为空) -->
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="app"></div>
<script src="/bundle.js"></script> <!-- 浏览器下载后执行 -->
</body>
</html>

2.2 优缺点

// 优点
✅ 开发体验好(本地开发快)
✅ 部署简单(静态资源 + CDN
✅ 交互体验好(页面切换无需重新加载)
✅ 服务器压力小

// 缺点
❌ 首屏白屏(需要等 JS 下载、解析、执行后才渲染)
SEO 差(爬虫看到的可能是空 HTML
❌ 首屏性能差(尤其是低端设备)

2.3 代表框架

  • React(CRA / Vite)
  • Vue(Vue CLI / Vite)
  • Angular

三、SSR(服务端渲染)

3.1 工作原理

用户请求 → 服务器执行 JS,生成完整 HTML → 返回给浏览器
→ 浏览器显示内容(首屏可交互)
→ 下载 JS → 执行 hydrate(水合)→ 接管交互
SSR 的核心是将页面的 HTML 生成工作从客户端浏览器转移到了服务器端。当用户发起请求时,服务器接收到请求后,会执行前端框架代码,将数据与模板整合,动态拼装成包含完整内容的 HTML 字符串,然后一次性返回给浏览器。浏览器接收后即可立即渲染首屏内容(FCP),随后下载并执行客户端 JS Bundle,通过 Hydration(水合)机制将事件监听器绑定到已有 DOM 节点上,使静态页面变为可交互的应用。
// Next.js 中的 SSR 示例(getServerSideProps)
// pages/about.js
export async function getServerSideProps(context) {
// 每次请求都会执行
const res = await fetch('https://api.example.com/data')
const data = await res.json()

return {
props: { data }, // 传给页面组件
}
}

export default function About({ data }) {
return <div>{data.title}</div>
}

3.2 优缺点

// 优点
✅ 首屏速度快(用户直接看到完整内容)
SEO 好(爬虫拿到完整 HTML
✅ 动态内容实时渲染(每次请求都重新生成)
✅ 社交分享友好(OG 标签完整)

// 缺点
❌ 服务器压力大(每个请求都要渲染)
TTFB 较长(服务器需要时间渲染)
❌ 开发复杂度高(需要 Node.js 服务器)
❌ 服务器成本高(需要能执行 JS 的服务器)

3.3 Hydrate(水合)过程

1. 服务器返回完整 HTML → 浏览器立即显示(可看到内容)
2. 浏览器下载客户端 JS
3. JS 执行,绑定事件监听器(hydration)
4. 页面变为可交互(Hydrated)
// React 水合示例
import { hydrateRoot } from 'react-dom/client'
import App from './App'

// hydrateRoot 接管服务器渲染的 HTML,添加事件绑定
hydrateRoot(document.getElementById('app'), <App />)

3.4 SSR 框架

框架 说明
Next.js React SSR 框架(最流行)
Nuxt.js Vue SSR 框架
Remix 全栈框架(基于 React)
SvelteKit Svelte SSR 框架
Angular Universal Angular SSR

四、SSG(静态站点生成)

4.1 工作原理

build 时 → 服务器执行所有页面的渲染 → 生成静态 HTML 文件
用户请求 → CDN 直接返回静态文件(无需服务器渲染)
SSG 的核心是在构建阶段(Build Time)预渲染页面。在运行构建命令(如 next build 或 nuxt generate)时,框架会抓取数据并生成包含真实 DOM 结构的静态 HTML、CSS 和 JS 文件。在生产环境中,这些文件会被部署到 CDN 上,用户访问时直接由边缘节点返回静态文件,无需服务器实时渲染。
// Next.js 中的 SSG 示例(getStaticProps)
// pages/blog.js
export async function getStaticProps() {
// 只在 build 时执行一次
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()

return {
props: { posts },
}
}

export default function Blog({ posts }) {
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)
}

4.2 动态路由的 SSG

// pages/blog/[id].js
// build 时生成所有可能的路径
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()

const paths = posts.map(post => ({
params: { id: post.id.toString() }
}))

return {
paths,
fallback: false, // 未预渲染的路径返回 404
}
}

export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.id}`)
const post = await res.json()

return {
props: { post },
}
}

export default function BlogPost({ post }) {
return <h1>{post.title}</h1>
}

4.3 fallback 选项

export async function getStaticPaths() {
return {
paths: [
{ params: { id: '1' } }, // 预渲染
{ params: { id: '2' } }, // 预渲染
],
fallback: false, // 未预渲染 → 404
// fallback: true, // 未预渲染 → 服务端渲染(SSR)未生成的路径在首次访问时,会先返回一个“加载状态(Fallback 页面)”,同时在后台生成静态 HTML。生成完成后,后续访问会命中缓存。
// fallback: 'blocking', // 未预渲染 → SSR + 缓存 未生成的路径在首次访问时,服务端会阻塞请求,等待 HTML 生成完毕后直接返回完整页面(用户体验更好,不会出现闪烁的加载状态)。
}
}

4.4 优缺点

// 优点
✅ 最快的加载速度(CDN 分发静态文件)
✅ 服务器压力最小(无需动态渲染)
SEO 最好(完整静态 HTML
✅ 安全性高(无服务器端代码执行)

// 缺点
❌ 内容不实时(build 时生成,更新需要重新 build)
❌ 路径数量多时 build 时间长
❌ 不适合频繁变化的内容
❌ 构建时间可能很长(数千个页面)

4.5 SSG 适用场景

  • 博客 / 文档站
  • 产品展示页
  • 营销落地页
  • 个人主页
  • 内容不常变化的官网

五、ISR(增量静态再生)

5.1 工作原理

build 时 → 生成静态 HTML(同 SSG)
用户请求 → 返回静态文件 → 同时检查是否过期
→ 未过期 → 直接返回
→ 已过期 → 后台重新生成 → 下次请求返回新版本
网站初始依然通过 SSG 生成。当用户请求某个页面时,如果数据已过期,服务器会返回旧的缓存版本,并在后台异步重新生成该页面的静态文件并更新缓存。
// Next.js 中的 ISR 示例
// pages/products.js
export async function getStaticProps() {
const res = await fetch('https://api.example.com/products')
const products = await res.json()

return {
props: { products },
revalidate: 60, // 每 60 秒最多重新生成一次
}
}

export default function Products({ products }) {
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}

5.2 revalidate 工作流程

时间线:
0s → build 生成静态页面
0-60s → 所有请求返回缓存的静态文件
60s → 第一个请求触发重新生成(stale)
60s+ → 请求返回旧缓存,后台重新生成
61s → 重新生成完成
61-120s → 请求返回新的静态文件
120s → 第一个请求再次触发重新生成
...

5.3 优缺点

// 优点
✅ 静态文件的性能(CDN 分发)
✅ 内容自动更新(无需手动重新部署)
✅ 服务器压力小(大部分时间返回缓存)
✅ 适合内容频繁变化的页面

// 缺点
❌ 内容不是实时的(有 revalidate 间隔)
❌ 首次访问未预渲染的页面会慢(需要 SSR
❌ 复杂度比纯 SSG
❌ 需要支持 Serverless 的平台(VercelNetlify 等)

5.4 ISR 适用场景

  • 电商产品列表(价格、库存变化)
  • 新闻聚合页
  • 用户生成内容的展示页
  • 需要 SEO 的动态内容页

六、四种方案决策树

需要 SSR / SSG / ISR 吗?

├── 不需要 SEO,不需要首屏性能
│ └── CSR(纯客户端渲染)

├── 需要 SEO + 首屏性能
│ │
│ ├── 内容几乎不变(博客、文档)
│ │ └── SSG(静态站点生成)
│ │
│ ├── 内容偶尔变化(每小时/每天)
│ │ └── ISR(增量静态再生)
│ │
│ └── 内容实时变化(每分钟)
│ └── SSR(服务端渲染)

└── 混合场景
└── Next.js / Nuxt.js 支持同一项目混用多种方案

七、Next.js 中的混用示例

// 同一个 Next.js 项目中,不同页面可以用不同方案

// 1. 静态页面(SSG)— 不需要任何配置
export default function About() {
return <h1>About Us</h1>
}

// 2. 动态数据页面(SSR)
export async function getServerSideProps() {
const data = await fetchDynamicData()
return { props: { data } }
}

// 3. 静态 + 定时更新(ISR)
export async function getStaticProps() {
const data = await fetchSemiDynamicData()
return {
props: { data },
revalidate: 300 // 5 分钟更新一次
}
}

// 4. 纯静态(SSG)
export async function getStaticProps() {
const posts = await fetchAllPosts()
return { props: { posts } }
}

八、性能指标对比

指标 CSR SSR SSG ISR
FCP(首次内容绘制) 差(1-3s) 好(<1s) 最好(<0.5s) 好(<1s)
LCP(最大内容绘制) 最好
TTI(可交互时间) 中等 最好
TTFB(首字节时间) 中等 最好
CLS(累积布局偏移) 中等 最好

九、Nuxt.js 中的对应方案

// Nuxt 3

// 1. CSR(默认)
// pages/index.vue — 默认客户端渲染

// 2. SSR(默认行为)
// Nuxt 3 默认 SSR,无需额外配置

// 3. SSG
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // 启用 SSR
})

// pages/blog.vue
export default {
async setup() {
const { data } = await useFetch('/api/posts')
return { posts: data.value }
}
}

// 或使用 routeRules 指定
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/blog/**': { prerender: true }, // SSG
'/admin/**': { ssr: false }, // CSR
'/products/**': { isr: 60 }, // ISR(60秒)
'/api/**': { cors: true, cache: { maxAge: 3600 } },
}
})

十、高频问题

Q1: SSR 和 SSG 的区别?

SSR:每次请求都实时渲染 HTML,内容永远是最新的
SSG:build 时一次性生成 HTML,内容是构建时的快照

SSR 适合内容频繁变化的页面(如社交 feed)
SSG 适合内容不常变化的页面(如博客文章)

Q2: ISR 的 revalidate 如何工作?

1. build 时生成静态页面
2. 用户请求 → 返回缓存的静态文件
3. 超过 revalidate 时间后,第一个请求触发后台重新生成
4. 请求仍返回旧缓存,用户无感知
5. 后台生成完成后,缓存更新
6. 下次请求返回新版本

Q3: SSR 为什么需要 Hydrate?

1. 服务器渲染的 HTML 只有内容,没有交互能力
2. 浏览器需要下载客户端 JS
3. JS 执行时,将事件监听器绑定到已有的 DOM 上(Hydrate)
4. Hydrate 完成后,页面才可交互

Hydrate 比重新渲染一遍 DOM 更快(避免了 DOM 创建开销)

Q4: SSR 的 TTFB 为什么比 CSR 差?

CSR:服务器直接返回静态 HTML(很快)
SSR:服务器需要执行 JS 生成 HTML(需要时间)

解决:使用流式 SSR(React 18 的 renderToPipeableStream)
→ 边生成边返回,不等全部完成

Q5: SSG 如何处理动态路由?

方法1:预渲染所有路径(getStaticPaths 返回所有可能的参数)
→ 适合路径数量有限的场景

方法2:fallback: true
→ 未预渲染的路径会 SSR 渲染,然后缓存

方法3:fallback: 'blocking'
→ 同 fallback: true,但会等待渲染完成再返回

Q6: ISR 和 SSR 如何选择?

ISR:内容不是实时的,但性能好(CDN 分发)
SSR:内容是实时的,但服务器压力大

选 ISR:内容每分钟变化可以接受(商品列表、新闻聚合)
选 SSR:内容必须实时(聊天室、实时数据看板)

Q7: React 18 流式 SSR 是什么?

// 传统 SSR:等所有组件渲染完再返回
renderToString(<App />) // 阻塞等待

// 流式 SSR:边渲染边返回
import { renderToPipeableStream } from 'react-dom/server'

renderToPipeableStream(<App />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
// 关键内容渲染完成,开始流式返回
response.pipe(res)
},
onAllReady() {
// 所有内容渲染完成
}
})

// 配合 React.lazy + Suspense 实现选择性注水

Q8: 水合不匹配(Hydration Mismatch)是什么?

// 服务器渲染
<div>Time: {new Date().toLocaleTimeString()}</div>
// 服务器时间:10:00:00

// 客户端水合
// 浏览器时间:10:00:03
// → 警告:Hydration mismatch

// 解决方案
// 1. 只在客户端渲染的组件
useEffect(() => {
setTime(new Date().toLocaleTimeString())
}, [])
return <div>Time: {time}</div>

// 2. suppressHydrationWarning
<div suppressHydrationWarning>{new Date().toLocaleTimeString()}</div>

Q9: 如何避免 SSR 中的内存泄漏?

// 问题:每次请求都创建新的 React 树
// 解决:复用 React 树

// Next.js 已自动处理
// 但自定义 SSR 需要注意:
// 1. 避免在模块级别保存请求相关状态
// 2. 使用请求级别的上下文
// 3. 及时清理副作用

// 错误示例
let globalState = {} // 所有请求共享 → 内存泄漏

// 正确示例
function handler(req, res) {
const context = {} // 每次请求独立
const html = renderToString(<App context={context} />)
}

Q10: SSG 的 Incremental Static Regeneration(ISR)和 On-Demand Revalidation 区别?

ISR(基于时间):
revalidate: 60 → 每 60 秒最多重新生成一次
自动触发,基于时间间隔

On-Demand Revalidation(按需更新):
revalidate('/') → 手动触发重新生成
适合内容变更时主动触发(如 CMS 发布新文章)
// On-Demand Revalidation 示例
// pages/api/revalidate.js
export default async function handler(req, res) {
await res.unstable_revalidate('/blog')
return res.json({ revalidated: true })
}

// 或使用 webhook
// pages/api/webhook.js
export default async function handler(req, res) {
if (req.body.secret === MY_SECRET) {
await res.unstable_revalidate(req.body.path)
return res.json({ revalidated: true })
}
return res.status(401).json({ error: 'Invalid token' })
}

十一、框架生态对比

框架 SSR SSG ISR 流式 SSR
Next.js ✅(React 18)
Nuxt 3 ✅(routeRules)
Remix ❌(嵌套路由)
Astro ✅(Islands)
SvelteKit
Gatsby ❌(已转向 SSR)

十二、一句话总结

方案 一句话
CSR 浏览器下载 JS 后渲染,适合后台系统
SSR 服务器每次请求都渲染,适合动态+SEO
SSG 构建时一次性生成,适合不常变化的静态内容
ISR SSG + 按需更新,兼顾性能和内容新鲜度
Streaming SSR 流式返回,边渲染边输出,减少 TTFB