SSR / SSG / ISR 原理
前言
在现代 Web 开发中,渲染模式的选择直接影响应用的性能、用户体验和 SEO 表现。从传统的客户端渲染(CSR)到服务端渲染(SSR),再到静态站点生成(SSG)和增量静态再生成(ISR),每一种模式都有其独特的适用场景和权衡。本文将系统性地深入讲解这四种核心渲染模式的工作原理、实现机制以及它们在主流框架中的配置方式。
一、CSR(客户端渲染)
1.1 什么是 CSR
客户端渲染(Client-Side Rendering)是指页面的 HTML 结构、样式和交互逻辑完全由浏览器端的 JavaScript 负责生成。服务器仅返回一个空的 HTML 外壳(通常是一个包含 <div id="app"> 的文档),所有内容都在浏览器中通过 JavaScript 动态创建和渲染。
1.2 CSR 工作流程
- 浏览器请求页面,服务器返回一个几乎空的 HTML 文档,其中包含一个根容器和
<script>标签引用。 - 浏览器解析 HTML,开始下载 JavaScript 文件(可能包括框架代码、业务代码和路由代码)。
- JavaScript 下载并解析完成后,框架(如 React、Vue)启动应用,创建虚拟 DOM。
- 框架将虚拟 DOM 渲染为真实 DOM,挂载到根容器上。
- 页面变得可交互(首次渲染到可交互之间可能存在时间差)。
1.3 CSR 的优点
- 丰富的交互体验:页面切换无需刷新整个页面,可以实现流畅的 SPA 导航体验。
- 后端解耦:前端可以独立开发部署,只需通过 API 与后端通信,前后端团队可以并行工作。
- CDN 友好:静态资源(HTML、JS、CSS)可以部署在 CDN 上,降低服务器压力。
- 开发体验好:成熟的工具链和热更新机制使得开发效率较高。
1.4 CSR 的缺点
- 首屏加载慢:用户需要等待所有必需的 JavaScript 下载、解析和执行完成后才能看到内容,尤其在弱网环境和低端设备上表现更差。
- SEO 不友好:搜索引擎爬虫可能无法正确执行 JavaScript,导致页面内容无法被索引。虽然 Google 的爬虫可以执行 JS,但效果仍不如服务端直接返回的 HTML。
- 白屏时间:从开始加载到页面首次出现内容的时间段内,用户看到的是完全空白的页面。
1.5 关键指标
白屏时间(First Paint / FP): 通常 1-3 秒(取决于 JS 体积和网络)
首屏渲染(FMP / LCP): 通常 2-6 秒
可交互时间(TTI): 通常 3-8 秒二、SSR(服务端渲染)
2.1 什么是 SSR
服务端渲染(Server-Side Rendering)是指在服务器上执行前端框架的渲染逻辑,生成完整的 HTML 字符串后直接发送给浏览器。浏览器收到的是已经包含内容的 HTML 文档,可以立即展示给用户。
2.2 SSR 工作流程
- 用户请求页面,请求到达 Node.js 服务器。
- 服务器运行前端框架的渲染函数,执行组件的生命周期逻辑,获取所需数据。
- 服务器将数据注入到组件中,渲染生成完整的 HTML 字符串。
- 服务器将 HTML 字符串发送给浏览器,其中包含页面内容以及嵌入的初始状态数据(脱水数据)。
- 浏览器接收 HTML 并立即渲染,用户可以看到页面内容。
- 浏览器下载 JavaScript 文件,执行水合(Hydration)过程,将事件处理器绑定到已有的 DOM 节点上。
- 页面变得可交互。
2.3 数据获取与同构
SSR 的核心在于同构(Isomorphic)——同一套代码既在服务器上运行,也在浏览器上运行。
在服务器端,组件需要预先获取数据。典型的数据获取模式如下:
// 服务器端数据获取示例(Next.js App Router)
async function Page() {
const data = await fetch('https://api.example.com/data');
const posts = await data.json();
return (
<div>
<h1>文章列表</h1>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}服务器端渲染时,fetch 请求在服务器上执行,返回的数据直接嵌入到 HTML 中。浏览器端水合时,不会再重新执行数据请求,而是使用嵌入的初始状态。
2.4 脱水和注水
- 脱水(Dehydration):服务器将获取到的数据序列化为 JSON,嵌入到 HTML 中的
<script>标签内(通常使用<script id="__NEXT_DATA__">或类似方式)。 - 注水(Rehydration/Hydration):浏览器加载 JS 后,直接从嵌入的 JSON 中读取初始状态,跳过再次请求,直接使用这些数据初始化应用状态。
2.5 SSR 的优点
- 更好的首屏加载性能:用户无需等待 JS 下载和执行即可看到完整的页面内容,白屏时间极短。
- SEO 友好:搜索引擎爬虫可以直接获取到包含完整内容的 HTML,搜索引擎的索引效果与传统的 MPA(多页应用)相当。
- 更好的 Core Web Vitals:特别是 LCP(最大内容绘制)指标显著优于 CSR。
- 弱网环境下体验更好:即使 JS 加载失败,用户仍然可以看到页面内容。
2.6 SSR 的缺点
- 服务器负载高:每次请求都需要在服务器上执行渲染,相比 CSR 的静态文件分发,服务器 CPU 和内存消耗显著增加。
- 响应时间变长:服务器需要等待数据获取和渲染完成才能响应,TTFB(首字节时间)可能比 CSR 更长。
- 网络延迟敏感:服务器需要连接数据源(数据库、外部 API),网络延迟会直接影响页面加载速度。
- 复杂度增加:需要处理服务器端特有的问题,如
window、document等浏览器 API 的缺失、Node.js 和浏览器环境差异等。
2.7 关键指标
TTFB(首字节时间): 通常 200-800ms(取决于服务器渲染时间)
白屏时间(First Paint): 通常 200-500ms
首屏渲染(FMP / LCP): 通常 500ms-2s
可交互时间(TTI): 通常 1-4s(取决于 JS 体积和水合时间)三、SSG(静态站点生成)
3.1 什么是 SSG
静态站点生成(Static Site Generation)是指在构建时(Build Time)预先将页面渲染为静态 HTML 文件。这些文件可以直接部署到 CDN 上,无需服务器端的实时渲染能力。
3.2 SSG 工作流程
- 在构建阶段,构建工具(如 Next.js、VitePress、Astro)遍历所有页面路由。
- 对于每条路由,执行对应的数据获取逻辑,获取页面所需的数据。
- 使用获取到的数据渲染完整的 HTML 页面。
- 将所有生成的 HTML 文件、CSS 文件、JavaScript 文件输出到构建目录。
- 将构建产物部署到静态文件服务器或 CDN。
- 用户请求时,CDN 或静态服务器直接返回预先生成的 HTML 文件,无需任何动态处理。
3.3 SSG 数据获取
// Next.js Pages Router 中 SSG 数据获取
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts },
// 可选:设置重新生成间隔
revalidate: 3600,
};
}3.4 CDN 部署
SSG 天然适合 CDN 部署。由于生成的是纯静态文件,CDN 可以在边缘节点缓存这些文件,用户从最近的节点获取内容,延迟极低。
用户 → CDN 边缘节点 → 返回 HTML
VS
用户 → 源服务器 → 动态渲染 → 返回 HTMLCDN 部署的另一个优势是源服务器几乎不承受请求压力——大部分请求被 CDN 拦截和响应。
3.5 SSG 的优点
- 极致的加载性能:HTML 文件直接从 CDN 返回,无需服务器渲染时间,TTFB 极低。
- 服务器成本低:无需动态服务器,可以部署在静态托管服务上(如 Vercel、Netlify、GitHub Pages)。
- 高可靠性:没有服务器运行时故障的风险,CDN 天然具备高可用性。
- 安全风险小:攻击面仅限于静态文件服务,没有服务器端代码注入的风险。
3.6 SSG 的缺点
- 构建时间长:对于拥有大量页面的站点,每次构建都需要重新生成所有页面。一个包含数万页面的博客站点可能花费数十分钟甚至数小时。
- 内容更新不及时:内容变化后需要重新构建和部署才能反映到线上。即使使用增量构建,也存在延迟。
- 不适合动态内容:依赖用户特定数据、实时数据或个性化内容的应用不适合全量 SSG。
- 构建时数据依赖:如果数据源在构建时不可用或出错,构建过程可能失败或生成不正确的页面。
3.7 关键指标
TTFB(首字节时间): 通常 50-200ms(CDN 边缘节点)
白屏时间(First Paint): 通常 100-300ms
首屏渲染(FMP / LCP): 通常 200ms-1s
可交互时间(TTI): 通常 500ms-2s
构建时间: 取决于页面数量,从秒到小时级别四、ISR(增量静态再生成)
4.1 什么是 ISR
增量静态再生成(Incremental Static Regeneration)是 Next.js 首创的一种混合渲染模式,它结合了 SSG 的性能优势和 SSR 的内容实时性。ISR 允许在部署后按需或定时重新生成单个页面,而无需重新构建整个站点。
4.2 ISR 工作流程
- 初始构建时,所有页面像 SSG 一样预渲染为静态 HTML。
- 设置每个页面的重新验证间隔(
revalidate时间)。 - 用户请求时,如果页面尚未过期,CDN 直接返回缓存的静态页面。
- 如果页面已过期,服务器在后台重新生成该页面的最新版本,同时仍然返回旧版本的缓存页面给用户(先旧后新策略)。
- 新版本生成后,更新缓存,后续请求将获得新版本的页面。
4.3 基于时间的重新验证
// Next.js ISR 配置
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts },
// 每 60 秒重新生成一次
revalidate: 60,
};
}4.4 On-demand Revalidation(按需重新验证)
Next.js 12.2+ 引入了按需重新验证机制,允许通过 API 路由触发页面重新验证,而不是等待时间间隔到期。
// API 路由:触发按需重新验证
export default async function handler(req, res) {
try {
// 验证请求身份(必须!)
if (req.headers['authorization'] !== `Bearer ${process.env.REVALIDATION_TOKEN}`) {
return res.status(401).json({ message: 'Unauthorized' });
}
// 触发指定页面的重新验证
await res.revalidate('/posts/1');
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).json({ message: 'Revalidation failed' });
}
}4.5 回退行为
ISR 提供三种回退行为模式:
fallback: false:未在getStaticPaths中预生成的路由返回 404 页面。适用于已知的有限页面集合。fallback: true:首次请求未预生成的页面时,服务器动态生成该页面的 HTML,并缓存以供后续请求。用户首次访问看到的是降级加载状态。fallback: 'blocking':首次请求未预生成的页面时,服务器在请求时同步生成页面,用户等待生成完成后再看到内容。SEO 最友好,但 TTFB 会变长。
4.6 ISR 的优点
- 兼具 SSG 的性能和 SSR 的实时性:热门页面被 CDN 缓存,但内容变更后可以快速更新。
- 支持大规模站点:无需全量构建即可更新单个或少量页面,极大减少构建时间。
- 按需更新:通过 On-demand Revalidation,内容管理系统(CMS)发布内容后可以即时触发页面更新。
- 优雅降级:回退机制确保新页面或未预生成的页面仍然可以被访问。
4.7 ISR 的缺点
- 需要考虑 Stale 内容:在重新验证间隔内,用户可能看到过时的内容。
- 实现复杂度较高:需要配置重新验证逻辑、回退行为和缓存策略。
- 仍然存在静态限制:不适用于高度个性化的用户定制内容。
- 服务器端仍需运行:需要有服务器组件来处理重新验证逻辑。
五、Hydration(水合)
5.1 什么是 Hydration
水合(Hydration)是指 SSR/SSG 生成的静态 HTML 在浏览器端被 JavaScript 激活的过程。服务器端只生成了 DOM 结构,但没有交互能力。浏览器下载并执行框架代码后,需要将事件处理器绑定到已有的 DOM 节点上,并恢复应用状态。
5.2 水合过程详解
- 服务器端渲染 HTML:服务器执行组件渲染逻辑,生成包含内容的 HTML 字符串,并嵌入脱水数据。
- 浏览器接收 HTML:浏览器解析 HTML,构建 DOM 树,用户可以看到页面内容。
- 下载 JavaScript 框架代码:浏览器开始下载应用所需的 JavaScript 文件。
- 创建虚拟 DOM:框架代码在浏览器中执行,根据脱水数据重新创建完整的虚拟 DOM 树。
- 对比与绑定:框架将虚拟 DOM 树与现有 DOM 树进行对比(Diff),为现有 DOM 节点绑定事件处理器。
- 应用状态恢复:框架恢复应用状态(全局状态、路由状态等)。
- 页面可交互:水合完成,页面变成可交互状态。
5.3 同构组件注意事项
同构组件需要在服务器端和浏览器端都正确运行,有以下关键点:
- 避免使用浏览器特有 API:
window、document、localStorage等在服务器端不存在,需要使用条件导入或动态导入。 - 数据获取必须一致:服务器端获取的数据必须与浏览器端水合时使用的数据一致,否则会出现水合不匹配错误。
- 副作用处理:服务器端不执行
useEffect/onMounted等副作用钩子,这些只在浏览器端执行。 - 时间相关代码:服务器端和客户端的
Date.now()返回值不同,需要特别注意时间格式的一致性。
5.4 水合常见问题
- 水合不匹配(Hydration Mismatch):服务器端 HTML 和浏览器端虚拟 DOM 不一致时,框架会报错并尝试重新渲染。常见原因包括:数据时间戳不一致、环境变量差异、浏览器扩展修改 DOM。
- 水合开销:在大型页面上,水合可能需要消耗大量 CPU 时间,导致 TTI(可交互时间)延迟。这也被称为"水合膨胀"问题。
六、流式 SSR(Stream)
6.1 什么是流式 SSR
传统的 SSR 需要服务器等待所有数据获取完成、所有组件渲染完成后才能发送完整的 HTML 响应。而流式 SSR 允许服务器一边渲染一边将 HTML 分块发送给浏览器,让浏览器可以提前开始处理部分内容。
6.2 Node.js Stream 与 Pipe
// Node.js 中 Stream 的基本概念
// 服务器端可以持续写入 HTML 片段
res.write('<html><body>');
res.write('<div>第一部分内容</div>');
// 等待异步数据...
setTimeout(() => {
res.write('<div>第二部分内容</div>');
res.write('</body></html>');
res.end();
}, 1000);在 React 18 中,流式 SSR 通过 renderToPipeableStream 实现:
import { renderToPipeableStream } from 'react-dom/server';
function handleRequest(req, res) {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
});
}6.3 流式 SSR 的工作流程
- 服务器开始渲染应用外壳(Shell)——非异步的、不需要等待数据的组件部分。
- 服务器将外壳 HTML 立即通过流发送给浏览器。
- 浏览器接收到外壳 HTML 后立即开始渲染,用户可以看到页面的布局骨架。
- 服务器等待异步数据(如 API 请求)完成后,渲染数据依赖的组件内容。
- 服务器将剩余的 HTML 通过流继续发送给浏览器。
- 浏览器逐步更新页面内容,填充数据。
6.4 流式 SSR 的优点
- 更快的 First Paint:浏览器无需等待完整响应即可开始渲染,首屏内容展示更快。
- 更好的用户体验:用户可以尽早看到页面的骨架结构和部分内容,感知性能显著提升。
- 与 Suspense 集成:React 18 的 Suspense 组件可以配合流式渲染,用加载状态(Spinner)标记尚未准备好的部分。
6.5 流式 SSR 的缺点
- 实现复杂:需要框架层面支持,对流式传输的状态码管理、错误处理、SEO 等都有额外要求。
- SEO 考量:搜索引擎爬虫可能需要完整 HTML 才能正确索引页面,流式传输可能影响部分爬虫的抓取效果。
- 调试困难:流式传输的响应不如一次性响应那样容易调试和日志记录。
6.6 渐进式 Hydration
流式 SSR 的进一步发展是渐进式 Hydration(Progressive Hydration)。在这种模式下,页面的不同部分可以独立水合,而不是等待整个页面水合完成。
- 按需水合:只对当前可视区域或用户即将交互的部分进行水合。
- 优先级排序:根据组件的重要性确定水合优先级,关键组件优先水合。
- 懒水合:某些组件(如下方不可见的内容)可以被延迟水合,直到用户滚动到该区域或即将与其交互。
七、四种渲染模式对比
以下表格从多个维度对比 CSR、SSR、SSG 和 ISR 四种渲染模式:
| 维度 | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| 渲染时机 | 浏览器运行时 | 每次请求时 | 构建时 | 构建时 + 按需 |
| TTFB | 低(静态 HTML) | 中-高(取决于渲染时间) | 极低(CDN) | 极低(CDN,除非回退) |
| First Paint | 慢(等待 JS) | 快(流式或完整 HTML) | 极快 | 极快 |
| LCP | 中-慢 | 快 | 极快 | 极快 |
| TTI | 慢(水合开销) | 中(等待 JS 水合) | 中(等待 JS 水合) | 中(等待 JS 水合) |
| SEO | 差 | 好 | 好 | 好 |
| 服务器负载 | 无 | 高 | 无(构建时) | 中(重新验证时) |
| 内容实时性 | 实时 | 实时 | 构建时快照 | 近实时 |
| 部署复杂度 | 低 | 高 | 低 | 中 |
| 适用场景 | 管理后台、工具型应用 | 电商详情、社交动态 | 博客、文档站、营销页 | 电商列表、CMS 内容站 |
选择指南
- 选择 CSR 的场景:管理后台、在线编辑器、需要大量交互的工具类应用,这些应用通常对 SEO 没有要求,但需要丰富的交互体验。
- 选择 SSR 的场景:电商、内容平台、社交网络、任何对 SEO 和首屏体验都有高要求的动态应用。
- 选择 SSG 的场景:技术文档、博客、企业官网、营销落地页,这些站点的内容相对固定,追求极致的访问速度和低运维成本。
- 选择 ISR 的场景:需要频繁更新内容的 CMS 站点、拥有大量页面的电商网站、需要平衡构建速度与内容实时性的场景。
八、框架中的渲染模式配置
8.1 Next.js(React)
Next.js 是目前支持渲染模式最全面的 React 框架。在 Pages Router 中:
// CSR(仅客户端渲染)
// 使用 useEffect 或 SWR 在客户端获取数据
function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(res => res.json()).then(setData);
}, []);
return <div>{data ? data.content : 'Loading...'}</div>;
}
// SSR(服务端渲染)
export async function getServerSideProps(context) {
const data = await fetch('https://api.example.com/data');
return { props: { data: await data.json() } };
}
// SSG(静态生成)
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data');
return { props: { data: await data.json() } };
}
// ISR(增量静态再生成)
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data');
return {
props: { data: await data.json() },
revalidate: 60, // 60 秒后重新生成
};
}App Router(Next.js 13+) 中的渲染模式更加灵活,通过组件层级的 'use client' 和 'use server' 指令来控制:
// App Router:默认是服务端组件
async function ServerComponent() {
const data = await fetch('https://api.example.com/data');
return <div>{data.content}</div>;
}
// 使用 'use client' 指令转为客户端组件
'use client';
function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}8.2 Nuxt.js(Vue)
Nuxt.js 在渲染模式方面的配置在 nuxt.config.ts 中:
// nuxt.config.ts
// SSR 模式(默认)
export default defineNuxtConfig({
ssr: true,
});
// SSG 模式(静态生成)
export default defineNuxtConfig({
ssr: true,
prerender: {
routes: ['/', '/about', '/posts'],
crawlLinks: true,
},
});
// CSR 模式(SPA 模式)
export default defineNuxtConfig({
ssr: false,
});
// 混合渲染模式(Nuxt 3.8+)
export default defineNuxtConfig({
routeRules: {
// 首页静态生成
'/': { prerender: true },
// 文章页面 ISR(每 60 秒重新验证)
'/posts/**': { swr: 60 },
// 用户页面 SSR
'/users/**': { ssr: true },
// 管理后台 CSR(SPA)
'/admin/**': { ssr: false },
},
});Nuxt 3 的 routeRules 功能允许在同一应用中为不同路由配置不同的渲染模式,这是实现混合渲染的优雅方式。
8.3 其他框架
- Astro:默认 SSG,支持通过
server输出模式启用 SSR,每个组件可以声明自己的渲染模式。 - SvelteKit:通过
+page.server.ts和+page.ts区分服务端和客户端数据加载,支持 SSG、SSR、CSR 混合。 - Remix:主要拥抱 SSR,但可以通过
loader和action的配置实现类似 SSG 的效果。 - Gatsby:纯 SSG 框架,通过 Gatsby Functions 和增量构建支持动态功能。
总结
选择合适的渲染模式是 Web 应用架构设计中的关键决策。没有一种模式适合所有场景——CSR 在交互密集的工具型应用中表现出色,SSR 适合对 SEO 和首屏体验有高要求的动态站点,SSG 在以内容为核心的静态站点中拥有最佳性能,而 ISR 则为需要在性能和实时性之间取得平衡的场景提供了优雅的解决方案。
理解每种模式的工作原理、性能特征和适用场景,能够帮助我们在实际项目中做出更合理的技术选型。更重要的是,现代前端框架正在模糊这些模式之间的界限,让同一应用中同时使用多种渲染模式成为可能——这就是混合渲染的趋势方向。