客户端呈现 HTML 和交互

使用 JavaScript 渲染 HTML 与渲染服务器发送的 HTML 不同,这可能会影响性能。本指南将介绍两者的区别,以及如何保持网站的渲染性能,尤其是在涉及互动时。

对于使用浏览器内置导航逻辑(有时称为“传统网页加载”或“硬导航”)的网站,浏览器默认情况下会很好地解析和呈现 HTML。此类网站有时称为多页应用 (MPA)。

不过,开发者可以根据应用需求,绕过浏览器默认设置。对于使用单页应用 (SPA) 模式的网站,情况确实如此,该模式会使用 JavaScript 在客户端上动态创建大部分 HTML/DOM。这种设计模式称为客户端渲染,如果涉及的工作量过大,可能会影响网站的 Interaction to Next Paint (INP)。

本指南将帮助您权衡使用服务器发送到浏览器的 HTML 与使用 JavaScript 在客户端上创建 HTML 之间的差异,以及后者如何在关键时刻导致较高的互动延迟。

浏览器如何呈现服务器提供的 HTML

传统网页加载中使用的导航模式涉及在每次导航时从服务器接收 HTML。如果您在浏览器的地址栏中输入网址或点击 MPA 中的链接,系统会执行以下一系列操作:

  1. 浏览器会针对提供的网址发送导航请求。
  2. 服务器以分块形式返回 HTML。

最后一步至关重要。它也是服务器/浏览器交换中最基本的性能优化之一,称为“流式传输”。如果服务器可以尽快开始发送 HTML,并且浏览器不等待整个响应到达,则浏览器可以在 HTML 到达时分块处理。

Chrome 开发者工具的“性能”面板中显示的服务器发送的 HTML 解析过程的屏幕截图。随着 HTML 流式传输,其区块会在多个较短的任务中进行处理,并且渲染是增量式的。
Chrome 开发者工具的“性能”面板中显示的服务器提供的 HTML 的解析和渲染。解析 HTML 并进行渲染所涉及的任务会被拆分为多个分块。

与浏览器中发生的大多数情况一样,解析 HTML 发生在任务中。当 HTML 从服务器流式传输到浏览器时,浏览器会通过在流式传输的数据块到达时逐块解析 HTML 来优化解析过程。这样一来,浏览器在处理每个数据块后都会定期让出主线程,从而避免长任务。这意味着,在解析 HTML 的同时,还可以执行其他工作,包括向用户呈现网页所需的增量渲染工作,以及处理在网页的关键启动期间可能发生的用户互动。这种方法可提高网页的 Interaction to Next Paint (INP) 得分。

要点总结:从服务器流式传输 HTML 时,您会免费获得 HTML 的增量解析和渲染,并自动让出主线程。而客户端呈现则无法做到这一点。

浏览器如何呈现 JavaScript 提供的 HTML

虽然对网页的每个导航请求都需要服务器提供一定量的 HTML,但有些网站会使用 SPA 模式。这种方法通常涉及服务器提供最少的初始 HTML 载荷,然后客户端会使用从服务器提取的数据组装的 HTML 填充页面的主要内容区域。后续导航(在这种情况下有时称为“软导航”)完全由 JavaScript 处理,以便使用新的 HTML 填充页面。

在更有限的情况下,客户端渲染也可能发生在非 SPA 中,即通过 JavaScript 将 HTML 动态添加到 DOM 中。

通过 JavaScript 创建 HTML 或向 DOM 添加内容有几种常见方法:

  1. innerHTML 属性允许您通过字符串在现有元素上设置内容,浏览器会将该字符串解析为 DOM。
  2. document.createElement 方法允许您创建要添加到 DOM 的新元素,而无需使用任何浏览器 HTML 解析。
  3. document.write 方法允许您将 HTML 写入文档(浏览器会解析该文档,就像在方法 1 中一样)。不过,由于多种原因,强烈建议不要使用 document.write。
Chrome 开发者工具的“性能”面板中显示的通过 JavaScript 呈现的 HTML 解析过程的可视化图表。该工作在单个长任务中进行,会阻塞主线程。
在客户端通过 JavaScript 解析和渲染 HTML,如 Chrome 开发者工具的“性能”面板中所示。解析和渲染该脚本所涉及的任务未分块,导致出现阻塞主线程的长任务。

通过客户端 JavaScript 创建 HTML/DOM 可能会产生重大后果:

  • 与服务器为响应导航请求而流式传输的 HTML 不同,客户端上的 JavaScript 任务不会自动分块,这可能会导致长任务阻塞主线程。这意味着,如果您在客户端上一次创建过多 HTML/DOM,网页的 INP 可能会受到负面影响。
  • 如果在启动期间在客户端上创建了 HTML,则浏览器预加载扫描器不会发现其中引用的资源。这肯定会对网页的 Largest Contentful Paint (LCP) 产生负面影响。虽然这不是运行时性能问题(而是提取重要资源时的网络延迟问题),但您肯定不希望网站的 LCP 因绕过这项基本的浏览器性能优化而受到影响。

如何应对客户端渲染对性能的影响

如果您的网站严重依赖客户端渲染,并且您观察到字段数据中的 INP 值不佳,您可能会想知道客户端渲染是否与此问题有关。例如,如果您的网站是 SPA,则字段数据可能会显示负责大量渲染工作的互动。

无论原因是什么,您都可以探索以下一些可能的原因,帮助您恢复正常。

尽可能多地提供来自服务器的 HTML

如前所述,默认情况下,浏览器会以非常高效的方式处理来自服务器的 HTML。它会以避免长任务的方式拆分 HTML 的解析和渲染,并优化主线程总时间。这会缩短总阻塞时间 (TBT),而 TBT 与 INP 密切相关。

您可能需要依靠前端框架来构建网站。如果是这样,您需要确保在服务器上呈现组件 HTML。这样可以减少网站所需的初始客户端渲染量,从而带来更好的体验。

  • 对于 React,您需要使用 Server DOM API 在服务器上呈现 HTML。但请注意:传统的服务器端渲染方法使用同步方法,这可能会导致第一字节时间 (TTFB) 变长,以及后续指标(例如 First Contentful Paint [FCP] 和 LCP)变长。请尽可能确保您使用的是适用于 Node.js 或其他 JavaScript 运行时的流式传输 API,以便服务器可以尽快开始将 HTML 流式传输到浏览器。Next.js 是一个基于 React 的框架,默认提供许多最佳实践。除了在服务器上自动呈现 HTML 之外,它还可以为不根据用户上下文(例如身份验证)而更改的页面静态生成 HTML。
  • Vue 默认也会执行客户端渲染。不过,与 React 一样,Vue 也可以在服务器上呈现组件 HTML。请尽可能利用这些服务器端 API,或考虑为 Vue 项目使用更高级别的抽象,以便更轻松地实现最佳实践。
  • Svelte 默认在服务器上呈现 HTML,但如果您的组件代码需要访问浏览器专属命名空间(例如 window),您可能无法在服务器上呈现该组件的 HTML。尽可能探索替代方法,以免造成不必要的客户端渲染。SvelteKit(类似于 Next.js 之于 React)在 Svelte 项目中尽可能多地嵌入了最佳实践,因此您可以避免仅使用 Svelte 的项目中的潜在陷阱。

限制在客户端上创建的 DOM 节点数量

当 DOM 较大时,渲染它们所需的处理量往往会增加。无论您的网站是成熟的 SPA,还是由于 MPA 互动而向现有 DOM 注入新节点,都请考虑尽可能减小这些 DOM 的大小。这将有助于减少在客户端渲染期间显示该 HTML 所需的工作量,从而有望降低网站的 INP。

考虑使用在线媒体服务工作器架构

这是一项高级技术,可能无法轻松适用于所有用例,但它可以让您的 MPA 在用户从一个网页导航到下一个网页时,给人一种即时加载的感觉。您可以使用 Service Worker 在 CacheStorage 中预缓存网站的静态部分,同时使用 ReadableStream API 从服务器提取网页的其余 HTML。

成功使用此技术后,您无需在客户端上创建 HTML,但从缓存中即时加载部分内容会给用户留下网站加载速度很快的印象。使用这种方法的网站几乎可以像 SPA 一样,但没有客户端渲染的缺点。它还会减少您从服务器请求的 HTML 量。

简而言之,流式 Service Worker 架构不会取代浏览器的内置导航逻辑,而是补充该逻辑。如需详细了解如何使用 Workbox 实现此目的,请参阅使用流更快地加载多页应用。

总结

网站接收和呈现 HTML 的方式会影响性能。如果您依靠服务器发送网站运行所需的所有(或大部分)HTML,则可以免费获得很多好处:增量解析和渲染,以及自动让出主线程以避免长任务。

客户端 HTML 呈现会带来许多潜在的性能问题,这些问题在许多情况下是可以避免的。不过,由于每个网站的要求不同,因此无法完全避免这种情况。为了缓解因客户端渲染过多而可能导致的长任务,请确保尽可能多地从服务器发送网站的 HTML,对于必须在客户端渲染的 HTML,请尽可能减小 DOM 大小,并考虑采用替代架构来加快向客户端传送 HTML 的速度,同时利用浏览器为从服务器加载的 HTML 提供的增量解析和渲染。

如果您能尽可能减少网站的客户端渲染,不仅可以提高网站的 INP,还可以提高其他指标(例如 LCP、TBT),在某些情况下甚至可以提高 TTFB。