Skip to content
 

SSR 服务端渲染的原理 ​

更新: 9/10/2026字数: 0 字 时长: 0 分钟

理解 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)" 的过程:

  1. 用户发起请求: 用户在浏览器输入网址或点击链接,发送 HTTP 请求到 Node.js 服务端。

  2. 服务端数据预获取 (Data Fetching): 服务端路由匹配到当前页面后,在 Node.js 环境中提前调用 API 接口或查询数据库,获取页面所需的初始化数据。

  3. 渲染组件为 HTML 字符串:(renderToString) 服务端将获取到的数据注入前端组件(如 React / Vue),使用框架提供的 renderToString() 等 API,把虚拟 DOM 结构转换成真实的 HTML 纯文本字符串。

  4. 返回 HTML + 预挂载数据: 服务端将渲染好的 HTML、样式表(CSS)以及一小段脱水数据(Dehydrated Data,如 window.__INITIAL_STATE__)打包返回给浏览器。此时浏览器收到响应即可立即把页面绘制出来(首屏可见,FCP)。

  5. 客户端"水合/注水" (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. 总结一览表(按执行先后顺序) ​

序号资源/动作浏览器在干什么用户能感知到什么
1HTML 文本建立连接,接收服务器发来的纯文本标签页开始转圈,白屏
2CSS 样式下载并解析样式规则依然白屏(被 CSS 阻塞)
3渲染首屏结合 HTML + CSS 生成渲染树整页刷出!眼睛看到内容 (FCP)
4JS Bundle在后台下载 JavaScript 脚本能看内容,但点击无响应
5水合 (Hydration)运行 JS,把点击事件绑定给 HTML DOM页面突然“活了”
6交互就绪激活全部绑定完全可以正常操作 (TTI)

六、总结:什么时候用 SSR 才是真正“变快”? ​

SSR 不是为了解决“所有性能问题”,它主要解决两个特定痛点:

  1. SEO (搜索引擎优化): 爬虫必须直接拿到完整 HTML。
  2. 首屏可见时间 (FCP) 极其重要:在弱网环境(如 3G)或低端设备下,用户无法等待庞大的 JS Bundle 下载。SSR 让他们能更快看到内容,心理感觉“变快了”,即使交互依然需要等待。

如果你的应用:

  • 不需要 SEO(例如:内部系统、ERP)。
  • 主要数据都在登录后获取。
  • JS Bundle 并不算巨大(< 300KB minified)。

那么继续使用 CSR 可能会获得更好的用户体验和更低的开发/服务器成本。 不要为了 SSR 而 SSR。