Fix JavaScript performance title. For modern web apps subtitle. The background image is a path through a forest.
赞助

修复您网站的 JavaScript 性能

阅读时间 9 分钟

在 Web 性能方面,您可能会想到压缩、资源优化甚至 HTTP 缓存等技术。这些确实很重要,并且有大量的现有资源涵盖了修复或实现它们的方法。然而,一些较少被讨论的性能瓶颈会严重影响网站速度。在这篇文章中,我们将讨论三个通常源于低效 JavaScript 模式的问题。

  • 长任务: 独占主线程的 JavaScript 操作,导致用户界面无响应。
  • 大型包大小: JavaScript 代码过大,无法快速下载、解析和执行。
  • 水合问题: 将 JavaScript 功能附加到服务器渲染的 HTML 的过程。

虽然这些都不是新问题,但现代 Web 实践和 Web 框架会加剧这些问题,使它们再次受到关注。

注意: 这些问题也可能源于其他来源,例如 CSS。例如,长任务可能由需要很长时间才能匹配元素的复杂 CSS 选择器引起。然而,本文的重点是 JavaScript。

以下是关于每个瓶颈的更多信息,以及有关解决此类性能问题的高级说明。

长任务

当主线程活动持续不断,阻塞浏览器 50 毫秒或更长时间时,就会发生长任务。请注意,许多 JavaScript 和浏览器 渲染任务是在主线程上顺序执行的。

当主线程繁忙时,它无法处理用户交互,也无法执行其他重要的渲染任务。这可能导致页面响应出现明显的延迟。

Long tasks on a request waterfall chart. The long tasks affect page rendering

让我们对一个 Next.js 页面运行 网站速度测试。此页面已服务器渲染,然后在客户端进行水合。有趣的是,尽管 HTML 有很大的负载,但页面显示用户很早就看到了有用的内容。

捕获此页面加载的基于实验室的测试显示了可接受的最大内容绘制 (LCP) 时间,并且没有累积布局偏移 (CLS)。这个简化的图表显示了当用户在长任务期间进行交互时会发生什么。

User interaction during a long task can delay the page updating

这里的问题是,即使内容可以及时加载,由于浏览器忙于解析和执行 JavaScript,页面很长时间都无法交互。在缓慢的水合过程中,用户看到了似乎是交互式的但实际上不是的内容。这意味着当用户试图与页面交互时,页面看起来是“冻结”的。

这个例子特别有趣,因为它强调了解析和执行时间,而不仅仅是下载时间。

如何缩短您的长任务

  1. 将工作让给主线程: 将您的工作分解成更小、可管理的任务。这有效地让浏览器得到休息,使其能够处理用户交互和渲染。您可以在我们关于使用调度器 API 的文章中了解更多关于此技术的信息。

  2. 使用 Web Worker: 虽然它可能不会让您的代码运行得更快,但Web Worker允许您在后台运行 JavaScript,从而使您的主线程可以自由地进行用户交互和渲染。

  3. 优化您的渲染模式: 实施技术,例如

作为一项通用的性能策略,您应该最大限度地减少主线程工作和活动。

大型包大小

大型包大小是指用户访问您的网站时需要下载、解析和执行的 JavaScript 代码的总量。“包”是一个 JavaScript 文件,其中包含您的应用程序代码以及任何依赖项。过大的 JavaScript 包会导致一些问题:

  • 较低的缓存命中率: 大文件更有可能失效并被重新下载,因为它们更容易发生更改。这会降低缓存的好处。
  • 更慢的下载时间: 大文件下载时间更长,尤其是在连接速度较慢的情况下。
  • 解析和执行时间增加: 这一点很容易被忽视:即使在快速的网络连接下,解析和执行大型 JavaScript 文件也需要大量时间,尤其是在移动设备上。缓慢的解析和执行时间也会导致长任务。

最后一点也是用户交互的问题。用户期望立即获得反馈,但如果浏览器忙于解析和执行 JavaScript,它就无法响应用户输入。

A page with two large JavaScript bundles affecting page rendering

上一个截图显示了一个包含多个大型 JavaScript 包的页面。页面渲染被以下原因阻塞:

  • JavaScript 包的下载时间。
  • JavaScript 包的解析阻塞特性。
  • JavaScript 包的执行时间。

如何解决大型包大小问题

  • 实现摇树 以从最终包中删除未使用的代码。
  • 使用代码拆分 将您的应用程序分解成可以有选择地加载的更小的块。
  • 使用延迟加载 延迟非关键资源的加载,直到它们需要为止。
  • 删除未使用的代码,使用 Chrome DevTools 或 Edge DevTools 中的“覆盖率”面板来识别和消除未使用的代码。覆盖率与源映射兼容,因此您可以确切地了解哪个源文件未被使用,并且可以安全地删除/延迟加载。
  • 尽可能迁移到原生 Web 平台功能,从而减少对自定义 JavaScript 代码的需求。许多过去从头开始使用 JavaScript 实现的常见模式和功能现在都有了原生浏览器支持。这意味着您可以用更少的代码实现相同的功能。

以下是一些可以替换常见 JavaScript 实现的强大 Web 平台功能:

以下是一些您应该了解的其他 Web 平台功能。您可以开始尝试它们,但请注意它们尚未获得跨浏览器支持。如果您使用这些功能,您应该检查您的网站在尚不支持这些功能的浏览器上是否仍然可用

水合问题

JavaScript 包的大小曾经更容易理解。开发人员知道它们包含什么;它们包含开发人员自己的代码、框架代码和第三方库代码。在构建过程中,所有这些部分都合并到一个文件中(例如 app-v123.js)。这使得识别导致大文件大小的原因更加简单。

然而,随着时间的推移,JavaScript Web 框架引入了服务器端渲染。这种新方法产生了不同类型的 HTML 响应,导致了水合问题。

Web 开发中的水合是什么意思?

水合是将 JavaScript 功能附加到服务器渲染的 HTML 的过程,使 HTML 在客户端具有交互性。像 Next.js 这样的流行框架默认使用某些水合技术。

虽然水合可以补充服务器端渲染 (SSR) 功能,但它可能会带来性能挑战:

  • 文档大小增加: 一些框架将状态序列化为 HTML 源代码作为 JSON,导致初始负载膨胀,甚至复制数据。此外,内联的序列化状态会产生解析和执行成本,如前所述。
  • 交互延迟: 用户可能会经历一个令人沮丧的时期,在此期间页面可见但尚未交互。水合负载越大,延迟就越长。这有时被称为“恐怖谷”。
  • 浪费性的重建: 一些提供 SSR 功能的 JavaScript 框架实际上会导致 DOM 被构建两次——一次在服务器上,另一次在客户端水合期间。这种冗余会浪费资源并减慢交互时间。

您应该始终考虑流行的 JavaScript 框架和库对性能的影响。流行的框架的大量资源、教程和入门套件有时会导致开发人员选择它们,而没有充分考虑性能影响。

对于一些 Web 开发人员来说,使用 JavaScript 框架创建网站是默认选择,无论用例如何。框架的流行度并不总是转化为更好的用户体验,因此您必须平衡开发人员的易用性、可维护性和用户体验。

Size analysis of a Next.js page showing hydration issues

Debugbear 的HTML 大小分析器特别有用,因为它会分解 HTML 文档的大小,显示初始 HTML 负载的大小和水合负载的大小。上一个截图显示,大型 HTML 文档主要是由于:

  • 50kb 的段落文本(用户首先看到的内容)
  • 50kb 的 JSON 数据(水合负载)

JSON 数据与段落文本大小相同并非巧合。这是因为 JSON 数据是段落文本的序列化版本。这种重复是水合负载中的常见问题。

减轻水合问题的策略

以下是一些您可以探索的途径,以提高存在水合问题的 Web 应用程序的性能。您应该知道,使用这些技术中的任何一种都可能涉及重大的架构更改,在某些情况下,甚至可能需要迁移到不同的 JavaScript 框架。

  1. 渐进式水合/选择性水合: 优先水合应用程序最关键的部分,将不太重要的组件推迟到稍后。

  2. 岛屿架构 在静态内容“海洋”中实现交互性的“岛屿”,从而降低总体水合成本。使用岛屿架构,您通常使用指令来标记应该水合的岛屿。

  3. 可恢复性 而不是重建整个 DOM,探索能够更有效地“恢复”服务器渲染状态的框架。有时,页面加载时几乎不会发送 JavaScript,但在需要时(例如用户交互时)会获取并执行必要的 JavaScript。

服务器端渲染

考虑放弃复杂的框架,而是使用带有原生客户端 JavaScript 的服务器端渲染应用程序。

  1. 使用常规的超链接和表单提交进行导航。
  2. 实现视图过渡 API 以实现平滑的页面过渡。
  3. 仅在绝对必要时添加一层薄薄的 JavaScript。

例如,您可以使用

  • Hotwire 用于基本客户端功能。
  • Stimulus 用于更高级的客户端交互。
  • Turn 用于页面和组件之间的平滑过渡。

当您将这些工具与有效的缓存策略和页面预加载结合使用时,您就可以创建高性能的 Web 应用程序,而不会牺牲功能和用户体验。

衡量 JavaScript 性能如何影响真实用户

下次绘制交互 (INP) 指标衡量网站响应用户输入的响应速度。如果在交互后页面冻结半秒钟,那么页面将不会让用户感觉响应迅速。INP 可以使用事件计时 API在浏览器中计算,或使用像web-vitals.js这样的库进行计算。

当交互导致渲染延迟时,长动画帧 API 可以告诉您具体是哪些脚本负责。每次出现渲染延迟时,都会记录一个类型为 long-animation-framePerformanceEntry。您可以这样访问这些事件:

js
performance.getEntriesByType("long-animation-frame");

invokerType 告诉您脚本运行的原因。在下面的示例中,值为 classic-script,这意味着处理发生在脚本文件的初始评估过程中。

Screenshot of the browser console showing long animation frames, with the script invoker type and source URL highlighted.

DebugBear 的真实用户监控可以可视化您网站访问者的这些数据,显示在交互过程中不同脚本的运行时间。

您还可以看到用户与哪个页面元素进行了交互以及交互发生的时间。如果用户在加载后不久与页面进行交互,这通常会导致额外的延迟,因为页面的某些部分仍在加载,这将延迟事件处理程序。

Screenshot showing performance data for an INP interaction, with INP score and LoAF scripts on the left and the CSS selector and screenshot of the INP element on the right.

持续监控您网站的 INP 分数,可以帮助您了解您网站上的哪些页面经常显示响应缓慢的交互,以及不同的访问者群体如何随着时间推移体验您的网站。基于长动画帧数据,您还可以看到哪些脚本最常负责交互延迟。

Screenshot of DebugBear real user monitoring dashboard showing INP data.

总结

本文探讨了由过度 JavaScript 引起的三个鲜为人知的性能瓶颈:长任务、大型包大小和水合问题。我们研究了 Web 框架有时如何放大这些问题,导致加载时间变慢并对用户交互产生负面影响。最后,我们讨论了缓解这些瓶颈的方法以及您可以采取哪些措施来衡量慢速 JavaScript 如何影响您的访问者。

希望您发现这篇博文对您有所帮助,并能激发您构建更高效、高性能的网站,为用户提供更流畅的体验。

本文由DebugBear赞助。DebugBear 帮助网站开发人员提供更好的用户体验并通过 Google 的核心 Web 指标评估。获取详细的页面速度建议,并持续监控合成和真实用户页面速度指标。