文章目录
理解 SSR(Server-Side Rendering,服务端渲染)的核心,其实只需要记住一句话:将网页的 HTML 结构生成过程从"用户的浏览器"搬到了"后端的服务器"。
在单页面应用(SPA/CSR)时代,服务器只返回一个几乎为空的 HTML 壳子(如 <div id="app"></div>)和一堆 JavaScript 文件,页面内容全靠浏览器运行 JS 动态渲染。
<div id="app"></div>
而 SSR 则是服务器直接拼接好包含完整内容的 HTML 字符串发送给浏览器,浏览器拿到就能直接显示。

一、SSR 的完整执行流程
现代前端框架(如 React / Vue)的 SSR 并不是简单的传统模板引擎,它经历了一个关键的 “服务端拼接 HTML + 客户端水合(Hydration)” 的过程:
-
用户发起请求:
用户在浏览器输入网址或点击链接,发送 HTTP 请求到 Node.js 服务端。 -
服务端数据预获取 (Data Fetching):
服务端路由匹配到当前页面后,在 Node.js 环境中提前调用 API 接口或查询数据库,获取页面所需的初始化数据。 -
渲染组件为 HTML 字符串:(renderToString)
服务端将获取到的数据注入前端组件(如 React / Vue),使用框架提供的renderToString()等 API,把虚拟 DOM 结构转换成真实的 HTML 纯文本字符串。 -
返回 HTML + 预挂载数据:
服务端将渲染好的 HTML、样式表(CSS)以及一小段脱水数据(Dehydrated Data,如window.__INITIAL_STATE__)打包返回给浏览器。此时浏览器收到响应即可立即把页面绘制出来(首屏可见,FCP)。 -
客户端"水合/注水" (Hydration):
浏览器下载并执行打包好的 JavaScript Bundle,框架(React/Vue)在客户端重新扫描现有 DOM,将事件监听器(如点击、输入事件)绑定到静态 DOM 节点上,把"干瘪"的静态 HTML 激活为可交互的动态应用。
二、核心概念:什么是"水合"(Hydration)?
初学者最容易困惑的地方在于:既然服务器已经把 HTML 渲染好了,为什么还需要下载 JS 并在客户端再运行一遍?
比喻说明:
服务端传过来的 HTML 就像一具 “干瘪的塑像” ——它有完整的外形(DOM 结构和文字内容),但缺乏灵魂(点击没反应、没有状态变化)。
客户端下载 JS 后进行的 Hydration(注水/水合),就像是给塑像注入水分和血液。JS 将事件绑定回现有的 DOM 上,让它变成活生生、可交互的组件。
如果省略了 Hydration 阶段,用户虽然能看到页面,但点击按钮、切换 Tab 或提交表单都不会有任何响应。
详细介绍见这篇文章:什么是水合-hydration
三、CSR 与 SSR 核心对比
| 维度 | CSR(客户端渲染) | SSR(服务端渲染) |
|---|---|---|
| 首屏渲染速度 (FCP) | 较慢(需先下载并执行庞大的 JS 文件) | 极快(直接拿到完整 HTML 展示) |
| SEO 搜索引擎优化 | 较差(爬虫拿到的是空 HTML 壳) | 友好(爬虫可以直接爬取 HTML 内容) |
| 服务器负载 (CPU) | 极低(主要压力在用户浏览器) | 较高(每条请求都需要服务器计算拼接 HTML) |
| 开发复杂度 | 简单(无需处理跨端代码与生命周期) | 较复杂(需注意 window/document 在服务端不存在) |
四、为什么感觉SSR并没有很快?
SSR 服务端渲染的详细执行流程图

在实际开发中,SSR 的“快”是有条件的,而“慢”通常是普遍且由于以下几个关键因素导致的。
我们用 CSR 和 SSR 的耗时结构对比来解释:
1. 核心瓶颈转移:从“客户端忙”变成“服务端忙”
| 模式 | 核心瓶颈 (慢在哪里) | 表现 |
|---|---|---|
| CSR | 客户端 JS 执行 (耗尽用户手机的 CPU) | 用户看到一个白屏/Loading 很久,直到 JS 执行完才显示内容。 |
| SSR | 服务端网络请求 (TTI) + 服务端计算 | 浏览器发送请求后,转圈圈(等待响应)很久,然后页面突然一下子完整显示出来。 |
2. “感觉慢”的四个具体原因:
A. 服务端 API 请求的级联延迟 (TTFB 增加)
这是 SSR 慢的首要原因。
- CSR: 浏览器先拿到 HTML,再去请求数据。虽然白屏,但浏览器至少响应了。
- SSR: Node.js 服务器必须等待所有的初始化数据请求(可能包含 3 个 API 调用)都完成后,才能开始生成 HTML。如果某个后端 API 很慢,你的 SSR 服务端就会被阻塞,浏览器就会一直等待,表现为首字节时间 (TTFB) 极高,也就是用户看到的“转圈圈等待响应”阶段极长。
B. 服务端 CPU 密集型操作 (renderToString)
前端组件通常包含大量的循环、判断和 DOM 结构。在服务端使用 renderToString 把这一大堆虚拟 DOM 节点计算成静态字符串是非常消耗 CPU 资源的。在高并发下,Node.js 的单线程特性很容易成为瓶颈,导致每个请求的处理时间都被拉长。
C. 激活 (Hydration) 的成本
虽然 SSR 提前给了用户 HTML 内容(FCP 快),但它并没有省去客户端的 JS 执行成本。
- CSR: JS 负责数据请求、DOM 生成和事件绑定。
- SSR: 客户端 JS 需要执行一遍几乎完全一样的组件渲染流程(只是不重新生成 DOM,而是只绑定事件)。如果你的 JS Bundle 依然很大,用户即使看到了内容,依然需要等待 JS 下载并执行完(激活)后才能点击。从内容可见 (FCP) 到完全可交互 (TTI) 的这段时间差,用户体验实际上是变差的。
D. 缺乏合适的网络缓存
CSR 的核心资源(HTML 壳、JS、CSS)都是静态文件,可以完美利用 CDN 强缓存。
SSR 的 HTML 是动态生成的,针对不同用户甚至不同时间点内容都不同。如果你的 SSR 服务端没有做好 HTML 的缓存策略(例如:只对非登录用户缓存 1 分钟),每次请求都要全流程计算一遍,那速度必然远慢于直接读取 CDN 缓存文件。
五、SSR 页面初始化的资源加载顺序
这个细节非常关键!很多人搞不懂 SSR,就是因为没理清浏览器在“初始化”那一瞬间,HTML、CSS、JS 和静态资源(如图片、字体)到底是按什么顺序加载和执行的。
我们用一张浏览器时间轴流程图,把页面从“一片空白”到“完全能点”的所有资源加载顺序一次性说清楚。
1. SSR 页面初始化的资源加载全顺序
用户输入 URL
│
▼
┌────────────────────────────────────────────────────────┐
│ 1. [HTML 文本文件] │
│ 服务端返回的纯文本。 │
│ 浏览器拿到后,一边解析 DOM 结构,一边发现后续资源。 │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 2. [关键 CSS 样式文件 / <style> 标签] │
│ 【最高优先级阻断】 │
│ 浏览器停止渲染页面,必须先下载并解析 CSS, │
│ 确保页面出来时带样式,不会“乱码/排版错乱”。 │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 3. [首屏静态资源:首屏图片 / 字体] │
│ 【与 CSS 几乎并行/稍后下载】 │
│ HTML 里的 <img> 标签和字体文件开始下载。 │
└──────────────────────────┬─────────────────────────────┘
│
▼
🌟 节点一:首屏呈现 (FCP)
【用户眼睛看到了完整页面、文字和样式】
【但此时页面是“假”的,点击任何按钮都没反应!】
│
▼
┌────────────────────────────────────────────────────────┐
│ 4. [客户端 JS 打包文件 (Bundle)] │
│ 【通常带 defer 或在 </body> 前】 │
│ SSR 虽然生成了 HTML,但仍需要下载打包好的 JS 文件。 │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 5. [JS 代码解析与执行 + 触发水合 (Hydration)] │
│ 浏览器在本地运行 JS,扫描已存在的 HTML DOM 节点, │
│ 把点击、输入等“事件监听器”绑定上去。 │
└──────────────────────────┬─────────────────────────────┘
│
▼
🌟 节点二:完全可交互 (TTI)
【水合完成!页面变成了活的,点击有响应了】
│
▼
┌────────────────────────────────────────────────────────┐
│ 6. [非首屏/次要静态资源] │
│ 【最低优先级】 │
│ 屏外图片(Lazy-load)、埋点统计 JS、广告脚本等。 │
└────────────────────────────────────────────────────────┘
2. 4 个非常容易混淆的加载细节
1. HTML 与 CSS:CSS 会“卡住”页面展示
- 细节:HTML 拿到了,浏览器不会立刻把字打印在屏幕上。如果
<head>里有外部 CSS 文件,浏览器会等 CSS 下载并解析完(生成 CSSOM)之后,才把带着漂亮的样式的页面一次性画出来。 - 为什么:如果 CSS 慢了,用户就会看到网页先是黑白乱码,突然闪烁一下变成漂亮页面(Flash of Unstyled Content,简称 FOUT),体验极差。
2. HTML 与 JS:JS 是什么时候下载的?
- 细节:在 SSR 中,JS 文件(React/Vue 客户端代码)通常放在 HTML 的最底部(
</body>之前),或者加上了defer属性。 - 为什么:不能让 JS 阻塞首屏渲染! 浏览器会先画出 HTML 内容(让用户看到字),然后在后台默默下载 JS。
3. 首屏可看 vs 页面可点:它们中间有一个“时间差”
-
这是 SSR 特有的现象:
-
节点一(FCP):HTML + CSS 处理完,页面出来了。
-
节点二(TTI):JS 下载完并跑完“水合(Hydration)”,页面才能点。
-
隐患:如果你的 JS 文件非常大,或者手机性能很差,这两个节点之间可能会相隔 2~3 秒。在这期间,用户看到按钮去点,却完全没有反应,会以为网页卡死了。
4. 图片等静态资源:不阻塞 DOM,但影响整体体验
- 细节:HTML 解析到
<img>时会发起图片下载,它不会阻塞 HTML 解析,也不会阻塞 JS 执行。 - 优化:现代 SSR 通常会对首屏图片做特殊处理(比如设置
priority预加载),而对首屏之外(往下滑动才能看到)的图片加上loading="lazy"(懒加载),等用户滑动到了再去下载。
如图所示:

3. 总结一览表(按执行先后顺序)
| 序号 | 资源/动作 | 浏览器在干什么 | 用户能感知到什么 |
|---|---|---|---|
| 1 | HTML 文本 | 建立连接,接收服务器发来的纯文本 | 标签页开始转圈,白屏 |
| 2 | CSS 样式 | 下载并解析样式规则 | 依然白屏(被 CSS 阻塞) |
| 3 | 渲染首屏 | 结合 HTML + CSS 生成渲染树 | 整页刷出!眼睛看到内容 (FCP) |
| 4 | JS Bundle | 在后台下载 JavaScript 脚本 | 能看内容,但点击无响应 |
| 5 | 水合 (Hydration) | 运行 JS,把点击事件绑定给 HTML DOM | 页面突然“活了” |
| 6 | 交互就绪 | 激活全部绑定 | 完全可以正常操作 (TTI) |
六、总结:什么时候用 SSR 才是真正“变快”?
SSR 不是为了解决“所有性能问题”,它主要解决两个特定痛点:
- SEO (搜索引擎优化): 爬虫必须直接拿到完整 HTML。
- 首屏可见时间 (FCP) 极其重要:在弱网环境(如 3G)或低端设备下,用户无法等待庞大的 JS Bundle 下载。SSR 让他们能更快看到内容,心理感觉“变快了”,即使交互依然需要等待。
如果你的应用:
- 不需要 SEO(例如:内部系统、ERP)。
- 主要数据都在登录后获取。
- JS Bundle 并不算巨大(< 300KB minified)。
那么继续使用 CSR 可能会获得更好的用户体验和更低的开发/服务器成本。 不要为了 SSR 而 SSR。
👋 感谢阅读!想了解更多?
📖 我的博客网站 | 记录思考,分享干货
🏡 我的个人主页 | 关于我、开源项目

1321

被折叠的 条评论
为什么被折叠?



