发布日期:2019 年 2 月 6 日,上次更新日期:2026 年 1 月 5 日
Web 开发者必须做出的核心决策之一是,在应用中实现逻辑 和渲染的位置。这可能很困难,因为构建网站的方式有很 多种。
我们对这一领域的理解源于过去几年在 Chrome 中与 大型网站的合作。一般来说,我们鼓励开发者 考虑使用服务器端渲染或静态渲染,而不是完全重新渲染 。
为了更好地了解我们在做出此 决定时选择的架构,我们需要 一致的术语 和每种方法的共享框架 。然后,您可以从网页性能的角度更好地评估每种渲染 方法的权衡取舍。
术语
首先,我们定义一些将要使用的术语。
渲染
- 服务器端渲染 (SSR)
- 在服务器上渲染应用以向客户端发送 HTML,而不是 JavaScript。
- 客户端渲染 (CSR)
- 在浏览器中渲染应用,使用 JavaScript 修改 DOM。
- 预渲染
- 在构建时运行客户端应用,以静态 HTML 形式捕获其初始状态。请注意,此处的“预渲染”与浏览器对未来导航的预渲染不同。
- 水合作用
- 运行客户端脚本,以便向 服务器渲染的 HTML 添加应用状态和互动性。水合作用假定 DOM 不会发生变化。
- 重新水合作用
- 虽然通常与水合作用的含义相同,但重新水合作用意味着 定期使用最新状态更新 DOM,包括在初始 水合作用之后。
性能
- 第一字节时间 (TTFB)
- 点击链接与 新网页上加载的第一个内容字节之间的时间。
- First Contentful Paint (FCP)
- 请求的内容(文章正文等)变为可见的时间。
- Interaction to Next Paint (INP)
- 一个代表性指标,用于评估网页是否始终能快速响应用户输入。
- Total Blocking Time (TBT)
- INP 的代理指标 ,用于计算网页加载期间主线程被阻塞的时间。
服务器端渲染
服务器端渲染会在服务器上生成网页的完整 HTML 以 响应导航。这样可以避免在客户端上进行额外的数据提取和 模板处理往返,因为渲染器会在浏览器 收到响应之前处理这些操作。
服务器端渲染通常会产生快速 FCP。在服务器上运行网页逻辑和 渲染可避免向客户端发送大量 JavaScript。 这有助于减少网页的 TTBT,从而降低 INP,因为 在网页加载期间,主线程不会经常被阻塞。当主线程被 阻塞的频率较低时,用户互动就有更多机会更快运行。
这是有道理的,因为使用服务器端渲染时,您实际上只是向用户的浏览器发送 文本和链接。这种方法适用于各种 设备和网络条件,并为有趣的浏览器优化 (例如流式文档解析)提供了机会。
使用服务器端渲染时,用户不太可能需要等待 受 CPU 限制的 JavaScript 运行后才能使用您的网站。即使您无法避免第三方 JavaScript,使用服务器端渲染来降低您自己的第一方JavaScript 费用也可以为您节省更多预算。不过,这种方法存在一个潜在的权衡取舍: 在服务器上生成网页需要时间,这可能会增加网页的 TTFB。
服务器端渲染是否足以满足您的应用需求,很大程度上取决于 您要构建的体验类型。关于服务器端渲染与客户端渲染的 正确应用,一直存在争论,但 您可以始终选择对某些网页使用服务器端渲染,而对其他网页不使用 。一些网站已成功采用混合渲染技术。 例如,Netflix 服务器会渲染其相对静态的着陆页,同时 prefetching 互动较多的网页的 JavaScript,从而让这些客户端渲染较多的网页有 更好的机会快速加载。
借助许多现代框架、库和架构,您可以在客户端和服务器上渲染同一 应用。您可以将这些技术用于 服务器端渲染。不过,在服务器和客户端上同时进行渲染的架构属于自己的解决方案类别,具有截然不同的性能特征和权衡取舍。React 用户可以使用 服务器 DOM API 或基于这些 API 构建的解决方案(例如 Next.js)进行服务器端渲染。Vue 用户可以使用 Vue 的 服务器端渲染指南 或 Nuxt。Angular 有 Universal。
不过,大多数热门解决方案都使用某种形式的水合作用,因此请注意您的工具使用的 方法。
静态渲染
静态渲染 发生在构建时。这种方法可提供快速 FCP,以及较低的 TBT 和 INP,前提是您限制网页上的客户端 JavaScript 数量。与 服务器端渲染不同,它还可以实现始终快速的 TTFB,因为 网页的 HTML 不必在服务器上动态生成。 一般来说,静态渲染意味着提前为每个网址生成单独的 HTML 文件 。由于 HTML 响应是预先生成的,因此您可以将静态 渲染部署到多个 CDN,以利用边缘缓存。
静态渲染的解决方案多种多样。Gatsby 等 Gatsby旨在让开发者感觉 自己的应用是动态渲染的,而不是作为构建步骤生成的。 11ty、 Jekyll 和 Metalsmith 等静态网站生成工具则采用其静态特性,提供更以模板为导向的方法。
静态渲染的缺点之一是,它必须为每个可能的网址生成单独的 HTML 文件。当您需要提前预测这些网址,以及对于包含大量唯一网页的网站时,这可能具有挑战性,甚至不可行 当您需要提前预测这些网址,以及对于包含大量唯一网页的网站时,这可能具有挑战性,甚至不可行
React 用户可能熟悉 Gatsby、 Next.js 静态导出或 Navi,所有这些工具都可以方便地从组件创建 网页。不过,静态渲染和预渲染的行为有所 不同:静态渲染的网页无需 执行大量客户端 JavaScript 即可实现互动,而预渲染则可以提高必须在客户端上启动的 单页应用的 FCP,才能使网页真正实现 互动。
如果您不确定给定的解决方案是静态渲染还是预渲染, 请尝试停用 JavaScript 并加载要测试的网页。对于静态 渲染的网页,大多数互动功能在没有 JavaScript 的情况下仍然存在。 预渲染的网页可能仍然具有一些基本功能(例如停用 JavaScript 的链接),但网页的大部分内容都是惰性的。
另一个有用的测试是使用 Chrome 开发者工具中的网络限制 ,并查看网页在实现互动之前下载了多少 JavaScript。 预渲染通常需要更多 JavaScript 才能实现互动,并且该 JavaScript 往往比 渐进式增强 方法更复杂。
服务器端渲染与静态渲染
服务器端渲染并非所有情况下的最佳解决方案,因为其
动态特性可能会产生巨大的计算开销。许多服务器端
渲染解决方案不会提前刷新、延迟 TTFB 或将发送的数据加倍
(例如,客户端上 JavaScript 使用的内嵌状态)。在 React 中,
renderToString() 可能会很慢,因为它是同步的且是单线程的。
较新的 React 服务器 DOM API
支持流式传输,可以在服务器上仍在生成 HTML 响应的其余部分时,更快地将 HTML 响应的初始部分发送到
浏览器。
“正确”进行服务器端渲染可能涉及查找或构建解决方案 以实现 组件缓存、管理 内存消耗、使用 记忆化 技术 以及其他注意事项。您通常需要两次处理或重建同一应用, 一次在客户端上,一次在服务器上。服务器端渲染更快地显示内容 并不一定意味着您需要做的工作更少。如果服务器生成的 HTML 响应到达客户端后,您需要在客户端上进行大量工作, 这仍然会导致网站的 TBT 和 INP 较高。
服务器端渲染会根据每个网址的需求生成 HTML,但它可能比仅提供静态渲染的内容慢 。如果您可以进行额外的 努力,服务器端渲染加上 HTML 缓存 可以显著缩短服务器渲染时间。服务器端渲染的优势 在于能够提取更多“实时”数据,并响应比静态渲染更完整的 请求集。需要个性化的网页 是静态渲染无法很好处理的请求类型的具体示例 。
在构建 PWA时,服务器端渲染也可能会带来有趣的决策。使用全页 Service Worker 缓存还是服务器渲染各个内容片段更好?
客户端渲染
客户端渲染是指使用 JavaScript 直接在浏览器中渲染网页。所有逻辑、数据提取、模板处理和路由都在 客户端上处理,而不是在服务器上处理。实际结果是,从服务器向用户设备传递更多数据,这会带来一系列权衡取舍。
客户端渲染可能难以在移动设备上实现和保持快速。
只需稍作努力,保持 JavaScript 预算紧张
并尽可能减少往返次数
来提供价值,您就可以让客户端渲染几乎复制
纯服务器端渲染的性能。您可以使用关键脚本和数据,让解析器更快地为您工作。我们还建议考虑使用 PRPL 等模式,以确保初始导航和后续导航感觉是即时的。<link rel=preload>
客户端渲染的主要缺点是,随着应用的增长,所需的 JavaScript 数量往往会增加,这可能会影响网页的 INP。 添加新的 JavaScript 库、 polyfill 和第三方代码时,这种情况尤其困难,因为它们会争夺处理能力,并且通常必须 在网页内容渲染之前进行处理。
使用客户端渲染并依赖大型 JavaScript 软件包的体验 应考虑积极的代码拆分 ,以降低网页加载期间的 TBT 和 INP,以及延迟加载 JavaScript,以便 仅在需要时提供用户所需的内容。对于互动性较少或 没有互动性的体验,服务器端渲染可以更好地解决这些问题 。
对于构建单页应用的人员,识别大多数网页共享的用户
界面核心部分,可以应用
应用 Shell 缓存
技术。结合 Service Worker,这可以显著提高
重复访问时的感知性能,因为网页可以非常快速地加载其
应用 Shell HTML 和依赖项。CacheStorage
重新水合作用结合了服务器端渲染和客户端渲染
水合作用 是一种通过同时进行客户端渲染和服务器端 渲染来缓解客户端渲染和服务器端渲染之间权衡取舍的方法。导航请求(例如完整网页加载或 重新加载)由将应用渲染为 HTML 的服务器处理。然后, 用于渲染的 JavaScript 和数据会嵌入到生成的文档中。 如果谨慎操作,这可以实现与服务器端渲染类似的快速 FCP,然后 在客户端上再次渲染时“拾取”。
这是一个有效的解决方案,但可能会带来相当大的性能 缺点。
使用重新水合作用的服务器端渲染的主要缺点是,即使它能提高 FCP,也可能会对 TBT 和 INP 产生显著的负面影响。服务器端渲染的网页看起来已加载并可互动,但实际上无法 响应输入,直到组件的客户端脚本执行完毕并且事件处理程序已 附加。在移动设备上,这可能需要 分钟时间,让用户感到困惑和沮丧。
重新水合作用问题:一个应用的价格,两个应用
为了让客户端 JavaScript 准确地接管服务器停止的位置, 而无需重新请求服务器渲染其 HTML 所用的所有数据,大多数 服务器端渲染解决方案会将来自界面数据 依赖项的响应序列化为文档中的脚本标记。由于这会复制大量 HTML,因此重新水合作用可能会导致的问题不仅仅是延迟互动。
服务器会返回应用界面的说明以响应
导航请求,但它也会返回用于组成该
界面,以及界面实现的完整副本,然后在
客户端上启动。在 bundle.js 加载和执行完毕之前,界面不会实现互动。
从使用服务器端渲染和 重新水合作用的真实网站收集的性能指标表明,它很少是最佳选择。最重要的原因 是它对用户体验的影响,即网页看起来已准备就绪,但其 互动功能均无法正常运行。
使用重新水合作用的服务器端渲染是有希望的。在短期内,仅对高度可缓存的内容使用服务器端渲染可以减少 TTFB,产生与预渲染类似的结果。以 增量、 渐进或部分方式重新水合作用可能是使这项技术在未来更 可行的关键。
流式服务器端渲染和渐进式重新水合作用
服务器端渲染在过去几年中取得了许多进展。
流式服务器端渲染
允许您以块的形式发送 HTML,浏览器可以在收到 HTML 时逐步
渲染。这样可以更快地向用户提供标记,从而加快 FCP。在
React 中,与
同步 renderToString() 相比,renderToPipeableStream() 中的流是异步的,这意味着可以很好地处理反压。
渐进式重新水合作用 也值得考虑 (React 已实现)。使用这种 方法,服务器渲染的应用的各个部分会随着时间的推移“启动” ,而不是像当前常见的做法那样一次性初始化整个 应用。这有助于减少使网页实现互动所需的 JavaScript 数量,因为它允许您延迟客户端升级网页的低优先级部分,以防止其阻塞主线程,从而让用户互动在用户发起后更快发生。
渐进式重新水合作用还可以帮助您避免最常见的
服务器端渲染重新水合作用陷阱之一:服务器渲染的 DOM 树被
销毁,然后立即重建,这通常是因为初始
同步客户端渲染需要的数据尚未完全准备就绪,通常是尚未解析的
Promise。
部分重新水合作用
事实证明,部分重新水合作用难以实现。这种方法是渐进式重新水合作用的 扩展,它会分析网页的各个部分 (组件、视图或树),并识别互动性较少或没有 反应的部分。对于每个主要静态部分,相应的 JavaScript 代码随后会转换为惰性引用和装饰性功能,从而将其客户端占用空间减少到几乎为零。
部分重新水合作用方法存在自身的问题和妥协。它 为缓存带来了一些有趣的挑战,而客户端导航意味着 我们无法假定应用惰性部分的服务器渲染 HTML 在没有完整网页加载的情况下可用。
三态渲染
如果 Service Worker 是您的选择,请考虑 三态 渲染。这项技术 允许您对初始导航或非 JavaScript 导航使用流式服务器端渲染,然后让 Service Worker 在安装后负责渲染 导航的 HTML。这样可以使缓存的组件和 模板保持最新,并为在同一会话中渲染新视图启用 SPA 样式的导航。当您可以在服务器、客户端网页和 Service Worker 之间共享相同的 模板处理和路由代码时,这种方法效果最佳。
SEO 注意事项
在选择 Web 渲染策略时,团队通常会考虑 SEO 的影响。 服务器端渲染是提供抓取工具可以解读的 "完整外观" 体验的热门选择。抓取工具可以理解 JavaScript, 但它们在渲染方面通常存在限制 。客户端渲染可以正常运行,但通常需要额外的 测试和开销。最近, 动态渲染 也成为值得考虑的选择,如果您的架构严重依赖 客户端 JavaScript。
总结
在决定渲染方法时,请衡量并了解瓶颈所在。 考虑静态渲染或服务器端渲染是否能 让您实现大部分目标。可以主要使用 HTML 并尽可能减少 JavaScript,以实现互动体验。下面是一个方便的信息图,展示了 服务器-客户端频谱:
鸣谢
感谢大家给出的评价和提供的灵感:
Jeffrey Posnick、Houssein Djirdeh、Shubhie Panicker、 Chris Harrelson 和 Sebastian Markbåge。