服务器端渲染和客户端渲染效果对比

为了更好的对比服务器端渲染和客户端渲染,开启低速模拟,来检验SSR对首屏加载速度的作用。

❗️以下示例均已关闭缓存,开启慢速3G模拟环境

服务器端渲染结果

服务器端渲染网络不佳的情况下,仍然能保证进入页面就可以获取首页的文章数据,用户体验较好。

20250820163813_rec_.mp4

客户端渲染结果

客户端渲染网络不佳的情况下,会有长达半分钟的首页白屏,会造成用户体验不佳。

20250820163858_rec_.mp4

上面的对比可以得出结论,在网络不佳的情况下,SSR是加载速度优化的重要手段,毕竟现在动辄几兆乃至几十兆的JS文件,在极端情况下可能造成长达数十秒的白屏时间,而SSR就可以通过体积较小的HTML来优化用户体验,让用户先看到一部分结果。

❓那么为什么客户端渲染会出现长时间的白屏而浏览器渲染不会呢?

服务器端渲染的历史

早期服务器端渲染

早期服务器端渲染流程:

  1. 浏览器发起请求,用户访问页面 URL,向服务器端发送请求。

  2. 服务器端接收到请求后,开始处理页面请求。

  3. 服务器端查询数据库获取所需数据。

  4. 服务器端将获取到的数据填入后端模板(如 PHP、ASP、JSP 等),生成 HTML 片段。

  5. 服务器端将各个 HTML 片段组合成完整的 HTML 页面。

  6. 完整的 HTML 页面返回浏览器。

  7. 浏览器接收到 HTML 后直接解析并渲染显示页面

❓为什么现代服务端渲染优于早期服务端渲染?

早期服务器端渲染每次路由跳转(比如提交表单,切换页面等),都会重新向服务器发起请求,服务端返回新的完整 HTML,浏览器清空原有 DOM,重新解析、渲染,因此整个页面会闪一下(白屏/闪屏)。

现代服务器端渲染的页面切换是 前端框架(React/Vue)控制的局部更新,不需要清空 DOM。例如导航栏,侧边栏等元素一般不会重新刷新。

  • 例如从 /home 切到 /about

    • 传统 SSR:重新请求 /about.html → DOM 清空重建 → 白屏。

    • 现代 SSR:React Router 拦截路由变化 → React 只更新必要的组件 → DOM 不被整体清空。

客户端渲染

客户端渲染流程如下:

  1. 浏览器请求 URL。

  2. 前端服务器端直接返回一个空的静态 HTML 文件,不需要查询数据库或做模板组装。HTML 文件中加载了渲染页面需要的 JavaScript 脚本和 CSS 样式表。

  3. 浏览器拿到 HTML 文件后开始加载脚本和样式表,并执行脚本。

  4. 脚本向后端服务请求 API 获取数据。后端服务器端查询数据库,获取数据并返回给js脚本。

  5. 获取到数据后,JavaScript 脚本将数据动态渲染到页面中,完成页面显示。

客户端渲染优点

  • 可以向用户快速展示页面的内容,增加用户体验

  • 给别人爬虫爬取相应的内容增加一定的困难

客户端渲染缺点

  • 可能需要向服务器端请求多次数据

  • 不利于SEO搜索引擎优化,即百度等搜索引擎搜索不到客户端渲染的数据

  • 存在白屏

服务器端渲染

服务器端渲染流程如下:

  1. 浏览器请求 URL,向前端服务器端发起请求。

  2. 前端服务器端根据不同的 URL,向后端服务器端请求数据。

  3. 前端服务器端获取数据后,生成包含具体数据的 HTML 文本,并返回给浏览器。

  4. 浏览器接收到 HTML,开始渲染页面内容。

  5. 浏览器加载并执行 JavaScript 脚本,为页面元素绑定事件,使页面可交互,该过程也被称为水合。

  6. 当用户在页面中交互(如跳转到下一个页面)时,浏览器执行 JavaScript 脚本并向后端服务器端请求数据。

  7. 浏览器获取到新数据后,再次执行 JavaScript 动态更新页面,而不是重新加载整个页面。

服务器端渲染的利弊

SSR 的优点

  • 更快的初始加载:首屏加载快,服务器端向客户端发送已完全渲染的页面,因此用户可以更快地查看内容。

  • 改进的 SEO:由于内容在 HTML 负载的第一个字节中存在,搜索引擎能够更轻松地抓取和索引内容。

  • 更好的低性能设备表现:由于大部分工作在服务器端上完成,几乎任何低性能的设备都能在短时间内绘制页面。

SSR 的缺点

  • 服务器端压力大:在 SSR 中,服务器端需要为每个用户请求生成完整的 HTML 页面,当并发请求量较高时,服务器端的 CPU 和内存资源消耗会显著增加,可能导致服务器端响应变慢甚至出现性能瓶颈,影响用户体验。

  • 开发维护复杂:由于需要在服务器端端处理页面渲染逻辑,涉及到服务器端端的技术栈和框架,增加了开发的复杂性。开发人员需要同时掌握前端和后端的知识,并且在出现问题时,调试和排查错误也相对困难,提高了开发和维护成本。

  • 首屏加载优化成本高:虽然 SSR 理论上能提高首屏加载速度,但如果页面中包含大量的动态数据和复杂的交互逻辑,服务器端端生成页面的时间可能会变长,反而影响首屏加载性能。而且为了实现更好的首屏加载体验,可能需要进行更多的优化工作,如代码拆分、缓存策略等,增加了开发的工作量。

  • 不利于跨平台:SSR 通常是基于特定的服务器端端技术和环境进行开发的,在跨平台和跨环境部署时可能会遇到兼容性问题,需要进行额外的适配工作。

❓为什么客户端渲染长时间白屏,而服务器端渲染不会

客户端渲染会白屏的原因

初始 HTML 是空壳

浏览器一开始拿到的 DOM 里面,#root 是空的。浏览器必须先下载bundle.js,解析bundle.js,执行React 的render()才能生成 DOM。如果bundle.js体积大、网络慢、JS 运行耗时,就会导致用户长时间看到白屏。页面不仅要渲染 DOM,还要绑定事件。在 CSR 里,必须等 JS 完全跑完,用户才能交互。

服务器端渲染不会白屏的原因

服务端渲染的HTML 一开始就带内容,SSR 返回的 HTML 已经是完整 DOM:

浏览器解析 HTML 的同时,直接把内容绘制出来 → 用户立刻看到页面。

JS 异步加载,即使 bundle.js 还没下载完,用户也能先看到“静态页面”。页面交互功能会在水合后补上,但用户不会面对空白屏幕。

混合渲染实例

本实例编写了一个博客网站,可以选择首页使用SSR渲染也可以选择首页使用CSR渲染,登陆及新增文章操作操作通过 CSR 完成。

服务器端渲染泳道图

客户端渲染泳道图

服务器端渲染的实现

服务器端渲染通常分为两个阶段:

  • 阶段一(服务端渲染阶段):在服务端生成 HTML → 提供快速的首屏渲染。

  • 阶段二(客户端水合阶段):在浏览器加载 JS → 让页面变成可交互应用。

服务器端主要步骤

脱水实现

概念:将组件的状态(state/props)序列化为 JSON,随 HTML 一起发送到客户端。

目的

  • 客户端在注水时可以复用状态,不必重新请求数据

  • 提升首屏渲染性能

流程

  1. 服务器端渲染组件 → 生成 HTML

  2. 获取组件内部状态(如 React state 或 Vue store)

  3. 序列化状态为 JSON → 嵌入 HTML(通常放在 <script> 标签里)

实现代码

步骤1:首先数据获取与格式化

步骤2:React组件渲染

步骤3:数据序列化和注入

此步骤会将序列化后的数据直接嵌入到HTML的script标签中

客户端主要步骤

注水实现

概念:客户端接收 SSR HTML 后,将其转换为可交互的组件

目的

  • 激活事件绑定(如点击、输入)

  • 恢复组件状态(从脱水 JSON 中读取)

  • 保持与服务器端渲染一致的 UI

流程

  1. 客户端 JS 解析 HTML

  2. 读取脱水的 JSON 状态

  3. 绑定事件监听器和生命周期函数

  4. 页面变为可交互

实现代码

步骤1:从服务器端注入的全局变量中恢复数据

步骤2:获取DOM容器

步骤3:创建React应用实例

步骤4:使用hydrateRoot进行水合

React水合的详细步骤

  1. 调用 hydrateRoot()

    • hydrateRoot() 初始化 React 根节点

    • 创建一个 Fiber Root(Fiber 树的根节点),关联已有 DOM

  2. 构建 Fiber 树

    • React 根据 JSX 构建虚拟 DOM

    • 创建 Fiber 节点

    • Fiber 节点和服务器端 HTML DOM 节点一一对应,形成 Fiber 树

  3. 对比已有 DOM(Reconciliation)

    • React 会把新建的 Fiber 树与现有 DOM 对比

    • 复用 DOM

      • 节点类型一致 → 复用原 DOM

      • 属性/内容不同 → 更新 DOM

      • 节点类型不同 → 删除旧 DOM,创建新 DOM

    • 避免整个页面重新渲染,提高性能

  4. 绑定事件

    • React 为复用的 DOM 节点 挂载事件处理函数

    • 例如 onClick, onChange

    • 原来静态的 HTML 此时变成可交互的 React 元素

  5. 执行生命周期和副作用

    • 类组件:

      • 调用 componentDidMount
    • 函数组件:

      • 执行 useEffect 中的副作用
    • 页面状态与 DOM 完全同步

  6. 激活更新机制

    • Fiber 树启动 调度机制

    • 后续状态更新(setState / useState)都会走 Fiber 的增量渲染

    • 页面从静态 HTML 变成真正的 SPA