在网页上呈现

我们应该在应用中的什么位置实现逻辑和渲染?是否应使用服务器端渲染?“Rehydration”如何?让我们来寻找一些答案吧!

艾迪·奥斯曼尼
Addy Osmani

作为开发者,我们经常需要遇到一些会影响整个应用架构的决策。Web 开发者必须制定的一项核心决策是,在应用中的什么位置实现逻辑和呈现。这可能很难做到,因为构建网站的方式有很多种。

我们对这一领域的了解源自于过去几年在 Chrome 中与大型网站交流的工作。从广义上讲,我们建议开发者考虑服务器端渲染或静态渲染,而非完全补水方法。

为了更好地理解我们在做出此决定时选择的架构,我们需要对每种方法有充分的了解,以及在谈论它们时要使用的一致术语。这两种方法之间的差异有助于从性能的角度说明在网页上进行渲染的利弊。

术语

渲染

  • 服务器端呈现 (SSR):将客户端或通用应用呈现为服务器上的 HTML。
  • 客户端呈现 (CSR):通过 JavaScript 在浏览器中呈现应用以修改 DOM。
  • 重构:在客户端上“启动”JavaScript 视图,使其能够重复使用服务器呈现的 HTML 的 DOM 树和数据。
  • 预渲染:在构建时运行客户端应用,以静态 HTML 形式捕获其初始状态。

性能

服务器端渲染

服务器端呈现为服务器上的网页生成完整的 HTML,以响应导航。这样可以避免在客户端上执行额外的数据提取和模板设置,因为系统会在浏览器收到响应之前进行处理。

服务器端渲染通常会生成快速 FCP。在服务器上运行网页逻辑和呈现可以避免向客户端发送大量 JavaScript。这有助于减少页面的 TBT,这也会导致 INP 下降,因为主线程在网页加载期间阻塞频率较低。如果主线程阻塞的频率较低,用户互动就会有更多机会更快地运行。这是合理的,因为使用服务器端呈现时,您实际上只是将文本和链接发送到用户浏览器。这种方法非常适用于各种设备和网络条件,并可实现一些有趣的浏览器优化,例如流式文档解析。

此示意图显示了影响 FCP 和 TTI 的服务器端渲染和 JS 执行。

使用服务器端呈现,用户不太可能会等待受 CPU 限制的 JavaScript 运行完毕,然后才能使用您的网站。即使在无法避免第三方 JS 的情况下,您也可以使用服务器端呈现来降低自己的第一方 JavaScript 费用,从而为其余代码分配更多预算。不过,此方法有一个可能的折衷之处:在服务器上生成网页需要花费时间,这可能会导致 TTFB 较高。

服务器端渲染是否足以满足您的应用在很大程度上取决于您打造的体验类型。对于服务器端呈现与客户端呈现的正确应用之间,存在着长期争论,但请务必注意,您可以选择对某些网页使用服务器端呈现,而不对另一些网页使用。一些网站采用了混合渲染技术并取得了成功。Netflix 服务器呈现其相对静态的着陆页,同时为互动较多的网页预提取 JS,从而使这些由客户端呈现的较大网页有更大的机会快速加载。

利用许多现代框架、库和架构,可以在客户端和服务器上渲染同一应用。这些技术可用于服务器端渲染。但请务必注意,同时在服务器和客户端上进行渲染的架构本身就属于一类解决方案,具有截然不同的性能特征和权衡。React 用户可以使用服务器 DOM API 或基于其构建的解决方案,例如用于服务器端渲染的 Next.js。Vue 用户可以查看 Vue 的服务器端渲染指南Nuxt。Angular 具备 Universal。不过,大多数流行的解决方案都会采用某种形式的水合方法,因此在选择工具之前,请注意所使用的方法。

静态渲染

静态渲染在构建时进行。此方法可提供快速 FCP,并且 TBT 和 INP 更低(假设客户端 JS 的数量有限)。与服务器端呈现不同,由于无需在服务器上动态生成网页的 HTML,因此该 API 还可以始终实现快速的 TTFB。通常,静态呈现意味着提前为每个网址生成单独的 HTML 文件。借助预先生成的 HTML 响应,可以将静态渲染部署到多个 CDN,以充分利用边缘缓存。

展示影响 FCP 和 TTI 的静态渲染和可选 JS 执行的示意图。

静态渲染的解决方案具有各种形状和大小。Gatsby 等工具旨在让开发者感觉他们的应用是动态渲染,而不是作为构建步骤生成的。静态网站生成工具(例如 11tyJekyllMetalsmith 均具有静态性质),因此采用了更加以模板为导向的方法。

静态呈现的缺点之一是,必须为每个可能的网址生成单独的 HTML 文件。如果您无法提前预测这些网址的具体内容,或者对于包含大量唯一网页的网站而言,这可能极具挑战性,甚至不可行。

React 用户可能熟悉 Gatsby、Next.js 静态导出Navi,所有这些都让使用组件编写网页变得很方便。不过,您有必要了解静态呈现和预呈现之间的区别:静态呈现的网页是交互式的,无需执行太多的客户端 JavaScript,而预呈现可以改善单页应用的 FCP;单页应用必须在客户端上启动,页面才具有真正的交互性。

如果您不确定给定的解决方案是静态呈现还是预呈现,请尝试停用 JavaScript 并加载要测试的网页。对于静态呈现的网页,在不启用 JavaScript 的情况下,大部分功能仍然存在。对于预渲染的网页,可能仍会存在一些基本功能(如链接),但大多数网页将处于非活动状态。

另一个有用的测试是使用 Chrome 开发者工具中的网络节流功能,并观察在网页进入可交互状态之前下载了多少 JavaScript。预渲染通常需要更多的 JavaScript 才能具有可交互性,而且 JavaScript 往往比静态渲染采用的渐进式增强方法更复杂。

服务器端呈现与静态呈现

服务器端渲染并不是万能的,其动态特性可能会带来巨大的计算开销。许多服务器端渲染解决方案不会过早刷新,可能会导致 TTFB 延迟,或发送的数据翻倍(例如,客户端上的 JavaScript 使用的内嵌状态)。在 React 中,renderToString() 可能很慢,因为它是同步且单线程的。新版 React 服务器 DOM API:支持流式传输,它可以更快地将 HTML 响应的初始部分提供给浏览器,同时让其他部分仍在服务器上生成。

为了“正确”服务器端渲染,可能需要寻找或构建组件缓存解决方案、管理内存消耗、应用记忆技术以及其他问题。您通常会多次处理/重新构建同一应用 - 一次在客户端上,一次在服务器上。服务器端渲染可以提早显示某些内容,但这并不意味着您的工作量就会减少。如果在客户端生成的 HTML 响应到达客户端后,您需要在客户端上执行大量工作,这仍然可能会导致网站的 TBT 和 INP 增加。

服务器端呈现会根据需要为每个网址生成 HTML,但与仅提供静态呈现内容相比,速度可能会慢一些。如果您能投入额外的精力,那么服务器端呈现与 HTML 缓存相结合可显著缩短服务器呈现时间。与静态呈现相比,服务器端呈现的优势在于能够提取更多“实时”数据并响应更完整的请求集。需要个性化的网页就是具体请求类型的一个具体示例,这类请求在静态呈现模式下无法正常呈现。

在构建 PWA 时,服务器端渲染也会做出一些有趣的决策:使用整页 Service Worker 缓存,还是单纯由服务器渲染个别内容,效果更好?

客户端渲染

客户端呈现是指使用 JavaScript 直接在浏览器中呈现网页。所有的逻辑、数据提取、模板和路由都是在客户端而不是服务器上处理的。有效的结果是,更多数据从服务器传递到用户设备,这有其自己的权衡取舍。

在移动设备上,客户端渲染可能难以实现和保持。如果只需完成极少量的工作,客户端渲染可以接近纯服务器端渲染的性能,从而保持紧凑的 JavaScript 预算,并通过尽可能少的往返实现价值。使用 <link rel=preload> 可以更快地传送关键脚本和数据,让解析器更快地完成工作。为了确保初始和后续导航能够即时执行,PRPL 等模式也值得评估。

此示意图显示了影响 FCP 和 TTI 的客户端渲染。

客户端呈现的主要缺点是,随着应用规模的扩大,所需的 JavaScript 数量往往也会增加,而这可能会对网页的 INP 产生负面影响。随着新的 JavaScript 库、polyfill 和第三方代码的添加,这一问题变得尤为困难,它们会争夺处理能力,通常必须先进行处理,然后页面内容才能呈现。

如果体验使用的是需要大型 JavaScript 软件包的客户端渲染,则应考虑激进式代码拆分,以在网页加载期间降低 TBT 和 INP,并确保延迟加载 JavaScript,即“仅在需要时提供您所需的内容”。对于互动很少或根本没有互动的体验,服务器端渲染可以代表这些问题的可扩展性更强的解决方案。

对于构建单页应用的人员来说,确定大多数页面所共享的界面核心部分意味着您可以应用 Application Shell 缓存技术。与 Service Worker 结合使用可以显著提升重复访问的感知性能,因为 App Shell HTML 及其依赖项可以非常快速地从 CacheStorage 加载。

通过 rehydration 结合使用服务器端渲染和客户端渲染

此方法尝试通过同时处理客户端渲染与服务器端渲染之间的权衡取舍。导航请求(例如,完整加载或重新加载)由将应用呈现为 HTML 的服务器处理,然后将用于呈现的 JavaScript 和数据嵌入到生成的文档中。谨慎操作后,此方法可实现快速 FCP,就像服务器端渲染一样,然后通过称为“(re)hydration”的技术在客户端上再次渲染,从而“拾取”数据。这是一个有效的解决方案,但可能存在相当大的性能缺陷。

使用 Rehydration 进行服务器端渲染的主要缺点是,即使能够提高 FCP,也会对 TBT 和 INP 产生显著的负面影响。服务器端呈现的网页可能看似已加载并可以互动,但在执行组件的客户端脚本并连接事件处理脚本之前,网页实际上无法响应输入。在移动设备上,这可能需要几秒甚至几分钟的时间。

或许您曾亲自遇到过这种情况:有一段时间,页面似乎已加载后,点击或点按没有任何反应。这种情况很快就会变得令人沮丧,因为用户开始思考为什么在尝试与网页互动时什么都不会发生。

补水问题:一款应用只买两个

补水问题通常比因 JavaScript 导致的互动延迟问题更严重。为了让客户端 JavaScript 能够准确地“接续”服务器停止的位置,而不必重新请求服务器用于呈现其 HTML 的所有数据,当前的服务器端呈现解决方案通常会将界面数据依赖关系中的响应序列化为脚本标记。生成的 HTML 文档包含大量重复内容:

包含序列化界面、内嵌数据和 bundle.js 脚本的 HTML 文档

如您所见,服务器返回应用界面的说明来响应导航请求,但它也返回用于构建该界面的源数据,以及界面实现的完整副本(随后会在客户端上启动)。只有在 bundle.js 完成加载和执行之后,此界面才会变为交互式界面。

使用服务器端呈现和重构 (rehydration) 功能从真实网站收集到的效果指标表明不应使用该功能。归根结底,原因在于用户体验:用户很容易会陷入“神秘山谷”,虽然页面看起来已经准备就绪,但是互动却没有。

展示客户端渲染对 TTI 性产生负面影响的示意图。

不过,通过 Rehydration 进行服务器端渲染是有希望的。从短期来看,仅对可缓存高度的内容使用服务器端渲染可以减少 TTFB,产生与预渲染类似的结果。逐步、逐步或部分补充水分可能是使这项技术在未来更加可行的关键。

流式服务器端渲染和渐进式重构

服务器端渲染在过去几年间有了许多改进。

流式服务器端呈现可让您分块发送 HTML,浏览器可以在收到数据块后逐步进行渲染。这可以导致快速 FCP,因为标记到达用户的速度会更快。在 React 中,与同步 renderToString() 相比,流在 [renderToPipeableStream()] 中异步,意味着背压得到妥善处理。

渐进式补液功能也值得考虑,React 现已推出。通过这种方法,服务器渲染的应用的各个部分将随时间的推移“启动”,而不是像当前常用的方法那样一次性初始化整个应用。这有助于减少使网页具有互动性所需的 JavaScript 数量,因为可以推迟对网页低优先级部分的客户端升级,以防阻塞主线程,从而让用户能在用户发起互动后更快地发生互动。

渐进式补位还有助于避免一种最常见的服务器端渲染补位陷阱,其中服务器渲染的 DOM 树会被销毁,然后立即重新构建,这通常是因为初始同步客户端渲染需要的数据尚未准备好,可能在等待 Promise 解析。

部分补水

事实证明,部分补液很难实现。这种方法扩展了渐进式补水 (Progressive rehydration) 思路,即对要逐步补充水分的各个部分(组件/视图/树)进行分析,并找出互动性很低或没有反应的部分。对于其中每个大部分是静态的部分,相应的 JavaScript 代码随后都会转换为 inert 引用和装饰功能,从而将其客户端占用空间减少到接近零。

部分水合方法自身也存在一些问题和折衷问题。这给缓存带来了一些有趣的挑战,而客户端导航意味着我们不能假设没有完全加载页面的情况下,应用闲置部分的服务器渲染的 HTML 也可使用。

三态渲染

如果您选择 Service Worker,则可能还对“三态”渲染感兴趣。通过此方法,您可以使用流式服务器端渲染进行初始/非 JS 导航,然后在安装 HTML 后让 Service Worker 进行导航的 HTML 渲染。这样可以使缓存的组件和模板保持最新状态,并实现 SPA 式导航,以便在同一会话中呈现新视图。当您可以在服务器、客户端页面和 Service Worker 之间共享相同的模板和路由代码时,此方法效果最佳。

三态渲染的示意图,显示浏览器和 Service Worker 与服务器通信。

搜索引擎优化 (SEO) 注意事项

在选择在网络上呈现的策略时,团队通常会考虑 SEO 的影响。通常选择服务器端呈现是为了提供抓取工具可轻松解读的“外观完整”的体验。抓取工具可能理解 JavaScript,但在呈现方式方面往往存在一些值得注意的限制。客户端渲染可以正常工作,但通常需要进行额外的测试和完成工作。最近,如果您的架构高度依赖客户端 JavaScript,动态呈现也已成为一个值得考虑的选项。

如有疑问,移动设备适合性测试工具非常有用,可帮助您测试所选方法的效果是否符合预期。通过该界面,您可以直观地预览 Google 抓取工具看到的任何网页、找到的序列化 HTML 内容(执行 JavaScript 后),以及呈现过程中遇到的所有错误。

移动设备适合性测试界面的屏幕截图。

总结

在决定渲染方法时,请衡量并了解有哪些瓶颈。考虑静态呈现或服务器端呈现能否助您取得最理想的成效。主要使用少量的 JavaScript 来提供 HTML 以获得互动体验,这是完全没有问题的。以下方便的信息图展示了服务器-客户端的范围:

显示本文所述选项范围的信息图。

赠金

感谢所有人的评价和灵感:

Jeffrey Posnick、Houssein Djirdeh、Shubhie Panicker、Chris Harrelson 和 Sebastian Markbåge