前端渲染模式的选择
2026年7月23日
在现代前端开发中,渲染模式的选择几乎是每个新项目一开始都必须面对的问题。因此,前端工程师需要了解有哪些不同的渲染模式,以及在什么场景下应该使用哪一种。在本期前端主题文章中,我们会介绍常见的渲染模式,以及不同模式各自适用的场景。
在继续阅读之前,建议先读过 E+ 前端主题文章中的 关键渲染路径 (critical rendering path)。因为本文涉及不少前置知识,这里不会逐一展开细节,所以先读完相关文章再回来阅读,会更容易有效吸收。
客户端渲染 client-side rendering (CSR)
从网页发展历史来看,早期前后端尚未明确分离时,页面通常由后端渲染并输出到前端,再由前端搭配 JavaScript 负责一些简单的交互与动画。不过,随着浏览器越来越强大,开始有能力在客户端完成复杂的运算与渲染;与此同时,客户端渲染也能带来更好的用户体验,于是业界开始发展客户端渲染 (client-side rendering,以下简称 CSR)。
CSR 模式下的渲染流程 在 CSR 模式下,请求发出后,浏览器会先取得一个空白的 HTML,作为最基本的骨架:
<html>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
接着,浏览器会根据 <script> 标签开始下载 JavaScript(上面的例子中是下载 bundle.js),再用 JavaScript 完成渲染,包括由 JavaScript 生成 DOM,并执行 关键渲染路径 (critical rendering path) 一文中提到的渲染流程。渲染完成后,客户端程序才会调用后端 API 获取数据;等数据拉取并呈现主要内容后,才会上报 Web Vitals 中的 LCP。
CSR 的优点 CSR 之所以出现,是因为前面提到的传统模式会由后端准备好所有内容再传给前端,每次跳转都需要重新加载整个页面,用户体验不够理想。而在 CSR 模式下,每当需要与后端交互时(例如点击按钮提交表单),不必让服务器返回一个全新的页面,因此不会产生整页跳转所带来的不佳体验。
除此之外,CSR 能将前后端代码划分得更清楚,不像传统模式那样容易把代码混在一起;从可维护性的角度来看,CSR 相对更容易维护。
CSR 的问题 虽然 CSR 可以带来更好的交互体验,但它也有一些明显的问题:
- 首次进入网站的等待时间较长:在 CSR 模式下,用户进入网站后,一开始看到的可能是全白或骨架屏 (skeleton),必须等 JavaScript 下载、解析、执行并更新 DOM 后,才能看到网站真正的内容。构建产物 (bundle) 越大,下载和解析所需的时间就越长,用户也要等得更久,这段等待过程的体验并不理想。
- SEO 相对较差:由于服务器一开始只会向客户端发送一个空白 HTML,搜索引擎爬虫最初抓到的也只是空白 HTML。即使目前许多搜索引擎会在抓取时执行 JavaScript,也无法保证每个搜索引擎都会按照预期执行,因此 SEO 效果仍存在不确定性。
如何解决 CSR 的问题
- 解决首次进入网站等待较久的问题:
- 尽量缩小构建产物的体积 (reduce bundle size)。业界常见的目标是将 gzip 压缩后的大小控制在 100 KB 以内。常见做法是通过代码分割 (code splitting) 搭配懒加载 (lazy loading),避免一次下载整包 JavaScript,只下载当前页面所需的代码。
- 使用预加载 (preload),尽可能提前下载需要的资源,以减少等待时间。
- 利用 Service Worker 的缓存 (cache),缓存 HTML、CSS 与 JavaScript。只要缓存尚未过期,用户下次访问同一页面时,就能省去前面的下载时间。
- 解决 SEO 相关问题:Vercel 先前的研究发现,Google 搜索引擎基本上会下载并执行 JavaScript,因此以 Google 搜索来说,CSR 的 SEO 问题并没有那么严重。不过,仍建议尽可能减少 JavaScript 体积,因为搜索引擎会为网站分配抓取预算 (crawl budget)。如果 JavaScript 体积过大、执行成本太高,预算耗尽后,部分页面可能无法执行 JavaScript,进而影响 SEO。
关于 CSR 的更多讨论,Client-side Rendering 这个开源项目(链接)做了非常详细的分析,有时间的话很值得一读。
服务器端渲染 Server-side rendering
介绍完 CSR 后,接下来让我们谈谈 SSR,也就是服务器端渲染 (server-side rendering)。由于 SSR 的具体实现方式在过去经历了不少变化,这里会把不同类型的 SSR 分开讨论。
传统 SSR
如前文所述,早期基本上没有明确的前后端分离,也没有 CSR。网页应用通常在服务器端完成所有处理,包括从数据库获取数据、渲染页面,再将结果传给客户端。
传统 SSR 的优点:与 CSR 相比,传统 SSR 不需要把整包 JavaScript 传到客户端,而是直接把渲染好的 HTML 发送给前端。这样一来,即使客户端使用的不是高端设备或高速网络,也不用担心 JavaScript 包过大而耗费很长时间下载与解析。同时,因为前端直接收到的是包含内容的 HTML,也不必担心 SEO 问题。
传统 SSR 的问题:传统 SSR 虽然有其优点,但 CSR 的兴起也有其原因。传统 SSR 每次发生操作时(例如提交表单后),整个页面都要重新加载,产生明显的页面跳转,用户体验不够理想。
SSR + 注水 (hydration)
这时你可能会问:有没有一种方法,既能保留传统 SSR 预先渲染 HTML 的优点,又能像 CSR 一样让后续操作由客户端接管,从而提供更好的用户体验?事实上,现代开发者使用的 SSR 正是采用这种方式。
以 Next.js 这类框架为例,它的 SSR 与传统 SSR 不同。它同样会先在服务器端渲染 HTML,但传到前端后,还会进行注水 (hydration),让后续操作不需要整页跳转,从而带来更好的用户体验。
具体流程如下:
- 服务器完成渲染后,将完整的 HTML 传到客户端,此时客户端已经能看到完整的网页内容。
- 接着,客户端下载少量 JavaScript,并在客户端绑定事件(也就是上面提到的注水 hydration)。因为初始画面已经存在,注水只需要根据现有 DOM 绑定事件处理器即可。
- 注水完成后,客户端就会像 CSR 一样接管页面。用户可以获得良好的交互体验,而不必像传统 SSR 那样经历整页跳转。
SSR + 注水的优点:它解决了传统 SSR 交互性不足的问题,也解决了 CSR 首次加载时会出现一段空白的问题。
SSR + 注水的问题:虽然这种方式看似同时解决了前两种模式的问题,但它仍有自己的缺点。具体来说,HTML 到达客户端后,画面可以立即显示,不会出现一段空白;然而,在 HTML 显示到 JavaScript 完成注水之间,页面虽然看得见,却无法交互。读者过去可能多少遇到过这种情况:网站画面已经出现,但按钮怎么点都没有反应。这正是 SSR + 注水的主要问题。
流式 SSR
针对 SSR + 注水的问题,业界近年来也提出了更好的解决方案,其中一种就是通过流式传输 (streaming) 实现 SSR。
SSR + 注水刚出现时,服务器通常会先渲染完整的 HTML,再一次性传给前端。但 HTML 可以通过流式方式传输,因此将流式传输与 SSR 结合后,服务器完成某个部分就能立刻渲染,并直接把已完成的部分发送给客户端。
这样一来,就不需要等所有数据都取得后才一次性渲染,也不用等服务器端完成全部渲染后才开始传输。对用户来说,体感上的加载体验会更好。
改良版注水
除了 SSR 本身可以通过流式传输优化之外,注水也是可以优化的方向。SSR 搭配注水刚出现时,最早的做法是一次为整个页面注水,因此必须等整个页面完成注水后才能交互。目前,业界已经发展出几种不同的注水优化方式。
渐进式注水 (Progressive Hydration) 渐进式注水可以指定哪些部分优先注水。页面刚加载时,只为重要部分注水,其他部分稍后再处理。先完成注水的部分就能立即交互,不必等整个页面都完成注水。
岛屿架构 (Island Architecture) 岛屿架构 (island architecture) 由 Etsy 架构师 Katie Sylor-Miller 提出。这个架构把页面拆分成不同组件(就像一座座岛屿),只有需要交互的部分才进行注水。与渐进式注水相比,可以把岛屿架构理解为分离式注水 (separately hydrated)。
其中一个区块的注水不会影响另一个区块。因此,如果某个组件需要较长时间处理,其他组件仍然可以先完成注水(而在渐进式注水中,如果前面的部分耗时较久,后续注水就会被阻塞)。
选择式注水 (Selective Hydration) 选择式注水由 React 团队提出。这个做法同样会将页面拆成不同组件,默认按照组件在 React 虚拟 DOM 树中出现的顺序逐一注水。同时,当用户与某个组件交互时,React 会暂停其他组件的注水,优先为用户正在交互的组件注水。这样,用户最先操作的部分就能优先恢复交互能力,体验也会更好。