修复网站的 JavaScript 性能

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

  • 长任务: 垄断主线程的 JavaScript 操作,导致用户界面无响应。
  • 捆绑包大小较大: JavaScript 代码太大,无法快速下载、解析和执行。
  • 水化问题: 将 JavaScript 功能附加到服务器呈现的 HTML 的过程。

虽然这些并不是新问题,但现代网络实践和网络框架可能会加剧这些问题,使它们再次成为人们关注的焦点。

注意: 这些问题也可能源于其他原因,例如 CSS。例如,复杂的 CSS 选择器可能需要很长时间才能匹配元素,从而导致任务执行时间过长。不过,本文的重点是 JavaScript。

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

长任务

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

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

请求瀑布图上的长任务。长任务会影响页面渲染

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

捕获此页面加载的实验室测试显示,最大内容绘制 ( LCP ) 时间尚可接受,且无累积布局偏移 ( CLS )。此简化图表显示了用户在执行长任务时进行交互时发生的情况。

长时间任务期间的用户交互可能会延迟页面更新

这里的问题是,即使内容能够及时加载,由于浏览器忙于解析和执行 JavaScript,页面也会长时间无法交互。在缓慢的“Hydration”过程中,用户看到的内容看似可交互,但实际上并非如此。这意味着,当用户尝试与页面交互时,页面会“冻结”。

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

如何缩短你的长期任务

  1. 让位于主线程: 将您的工作分解成更小、更易于管理的任务。这可以有效地让浏览器休息一下,让它专注于处理用户交互和渲染。您可以在我们关于使用 Scheduler API 的文章中了解更多关于此技术的信息。

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

  3. 优化渲染模式: 实施以下技术:

作为一般的性能策略,您应该尽量减少主线程的工作和活动。

大捆尺寸

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

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

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

包含两个大型 JavaScript 包的页面影响页面渲染

上图显示的页面包含大量大型 JavaScript 包。页面渲染受以下因素阻碍:

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

如何解决大包大小问题

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

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

以下是一些需要注意的 Web 平台功能。您可以开始尝试这些功能,但请注意,它们目前尚不支持跨浏览器。如果您使用这些功能,请检查您的网站在尚不支持这些功能的浏览器上是否仍然可用。

补水问题

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

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

Web 开发中的水合 (Hylation) 是什么?

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

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

  • 文档大小增加: 一些框架将状态以 JSON 格式序列化到 HTML 源代码中,导致初始负载膨胀,甚至数据重复。此外,如前所述,内联序列化状态会带来解析和执行成本。
  • 交互延迟: 用户可能会经历一段令人沮丧的时期,页面可见但无法交互。Hydration 负载越大,延迟时间越长。这有时被称为“恐怖谷”。
  • 浪费的重建: 一些提供 SSR 功能的 JavaScript 框架实际上会导致 DOM 被构建两次——一次在服务器端,另一次在客户端进行 hydration 操作。这种冗余会浪费资源,并降低交互速度。

您应该始终考虑流行的 JavaScript 框架和库对性能的影响。流行框架拥有丰富的资源、教程和入门工具包,这有时会导致开发人员在选择它们时没有充分考虑其性能影响。

对于一些 Web 开发者来说,无论用例如何,使用 JavaScript 框架创建网站都是默认选择。框架的流行并不一定意味着更好的用户体验,因此你必须在开发者工效学、可维护性和用户体验之间取得平衡。

Next.js 页面大小分析,显示水合问题

Debugbear 的HTML 大小分析器非常有用,因为它可以分解 HTML 文档的大小,显示初始 HTML 有效载荷的大小和混合有效载荷的大小。上图显示,HTML 文档过大主要是因为:

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

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

缓解补水问题的策略

对于存在数据融合问题的 Web 应用,您可以尝试以下几种方法来提升其性能。需要注意的是,使用这些技术可能需要进行重大的架构变更,在某些情况下,甚至可能需要迁移到其他 JavaScript 框架。

  1. 渐进式水合/选择性水合: 首先优先对应用程序中最关键的部分进行水合,将不太重要的组件推迟到稍后进行水合。
  2. 孤岛架构 在静态内容的海洋中实现交互“孤岛”,降低整体内容融合成本。使用孤岛架构,您通常使用指令来标记哪些孤岛需要进行内容融合。
  3. 可恢复性 与其重建整个 DOM,不如探索能够更高效地“恢复”服务器渲染状态的框架。有时,页面加载过程中几乎不会发送任何 JavaScript,但在需要时(例如用户交互时)会获取并执行必要的 JavaScript 。

服务器端渲染

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

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

例如,您可以使用:

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

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

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

交互到下一次绘制(INP) 指标衡量网站响应用户输入的速度。如果页面在交互后卡顿半秒钟,则用户会感觉页面响应迟钝。您可以使用事件计时 API 或web-vitals.js等库在浏览器中计算 INP 。

当交互导致渲染延迟时,长动画帧 API可以告诉您哪些特定的脚本导致了延迟。每次出现渲染延迟时,都会记录一个PerformanceEntry与该类型相关的事件long-animation-frame。您可以像这样访问这些事件:

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

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

浏览器控制台的屏幕截图显示了长动画帧,其中突出显示了脚本调用者类型和源 URL。

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

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

屏幕截图显示了 INP 交互的性能数据,左侧是 INP 分数和 LoAF 脚本,右侧是 CSS 选择器和 INP 元素的屏幕截图。

持续监控您网站的 INP 分数,可以让您深入了解网站上哪些页面经常出现交互响应不佳的情况,以及不同访客群体在一段时间内对您网站的体验。根据长动画帧数据,您还可以发现脚本是造成交互延迟的最常见原因。

DebugBear 真实用户监控仪表板的屏幕截图,显示 INP 数据。

概括

这篇文章探讨了 JavaScript 过载导致的三个鲜为人知的性能瓶颈:耗时任务、过大的包大小以及内存占用问题。我们分析了 Web 框架有时会如何放大这些问题,导致加载时间变慢并对用户交互产生负面影响。最后,我们讨论了缓解这些瓶颈的方法,以及如何衡量 JavaScript 运行缓慢对访问者的影响。