发布于 2026-01-05 0 阅读
0

JavaScript 应用性能概述 JavaScript 应用性能概述 目录 初始加载 JavaScript 执行 性能测量 结论

JavaScript 应用性能概述

JavaScript 应用性能概述

目录

初始负载

JavaScript 执行

绩效衡量

结论

JavaScript 应用性能概述

警告⚠ - 这篇文章篇幅较长,但我希望它能为那些希望提升应用程序性能的人提供有用的参考。

跑步者

网络性能是一个庞大而复杂的话题。性能涉及诸多方面,每个项目都有不同的需求,对性能的关注程度也会因项目目标而异。随着网络的发展和逻辑计算层的不断增加,性能很容易受到影响。

本文仅作浅尝辄止的概述,介绍一些可能影响性能的因素;不会深入探讨任何特定的性能领域或库,而是主要关注需要注意的不同性能方面。由于侧重于高层次的概述,因此文中也不会提供太多具体的代码示例。

前端性能总有一些方面是你无法控制的;尤其是在使用 JavaScript 时——性能最好的网站往往是那些很少或没有 JavaScript 的网站(尽管这对于许多网站的需求来说并不现实):

  • 客户端仍然需要下载网站资源。虽然你可以通过优化应用打包方式来缓解这个问题,但最终性能还是取决于网络速度。
  • JavaScript 经常需要与各种 API 交互。虽然网络速度也是一个影响因素,但 API 处理请求和发送响应所需的时间也会影响性能。

目录

  1. 初始负载
    1. 正在下载JS
    2. 捆绑销售
    3. 图片
  2. JavaScript 执行
    1. 记忆化
    2. 执行时间
    3. 记忆问题
    4. 卸货工作
    5. 渲染图
  3. 绩效衡量
    1. 绘画
    2. 衡量绩效的工具
  4. 结论

初始负载

应用性能最重要的影响因素之一是初始资源加载(/下载)所需的时间。一般来说,应用越复杂,需要加载的资源就越多。

资源下载对于网络速度和稳定性不如 4G 和 5G 的低端网络用户来说尤为重要。Speedtest全球指数可以让我们深入了解世界各地网络速度的差异。帮助提升应用程序的性能和加载速度,对于网络连接速度较慢的用户来说意义重大,也是确保网络尽可能无障碍访问的重要一步。

通过一种称为自适应服务的技术,开发者将越来越容易根据用户的连接速度以不同的方式提供应用程序服务。根据用户的连接速度,发送给用户的资源会进行调整(例如,提供高质量视频而不是低质量视频)。

有大量统计数据表明初始加载时间的重要性,以下是一些重点:

  • 53%的移动用户会放弃加载时间超过3秒的网站——谷歌,2016年
  • 页面加载时间每增加一秒,就会有10%的用户离开——BBC,2016年
  • 根据谷歌神经网络预测模型,当页面加载速度从 1 秒降至 5 秒时,跳出率会增加 90%——谷歌,2017 年

Shaun Anderson整理了一份详尽的资源和统计数据清单,详细介绍了快速加载时间的重要性和好处。

如果以上统计数据还不足以让你相信初始加载速度的重要性,那么据报道,提高网站的加载速度也会对其SEO 排名产生积极影响,尽管谷歌尚未透露这种影响有多大。

提高网页加载速度的一个简单方法是利用缓存。资源/资产缓存的两种主要方式是通过[此处应填写缓存方式名称]browser caching和[此处应填写缓存方式CDN caching名称]。

浏览器缓存

用户浏览器会将资源缓存在此处以便下次同一用户访问该网站时,可以直接从本地数据而非通过 HTTP 获取资源。由于资源存储在本地,因此用户必须先访问网站,资源才能被缓存。

开发者们越来越能够对缓存哪些资源以及在何种情况下使缓存失效进行更精细的控制。谷歌的Workbox就是一个很好的例子,它提供了一个 API 来实现这一点。

用户可以随时选择删除本地/浏览器缓存。

CDN缓存

缓存是内容分发网络 (CDN) 的主要优势之一。与浏览器缓存类似,CDN 缓存旨在存储特定应用程序的资源。主要区别在于,CDN 将资源存储在地理位置靠近用户的服务器上,而不是存储在用户的本地计算机上——这意味着资源传输距离更短,从而使用户能够更快地访问应用程序的内容。

链接预取

浏览器能够预取用户将来可能需要的特定资源,并将其存储在缓存中。这意味着当用户访问某个预取资源时,可以快速从缓存中检索,从而提升性能并降低对稳定网络连接的依赖。Ivan Akulov 撰写了一篇精彩的文章<link rel>,介绍了可用于通过预先检索资源来提升性能的各种标签。

正在下载JS

下载所需 JavaScript 的方式会对性能产生连锁反应。理想情况下,应该先下载最初几个操作所需的 JavaScript,然后延迟下载其他 JavaScript,从而尽可能地提升用户体验的流畅度。

在 HTML 中嵌入/引用脚本时,可以使用一些注重性能的属性(如下图所示):

  • script- 使用普通的 script 标签会在脚本下载和执行期间阻止 HTML 解析。
  • script asyncHTML5 新增了一个async属性,允许在解析代码的同时下载 JavaScript 文件。如果与 JavaScript模块脚本一起使用,则会并行获取整个依赖树。JavaScript 文件下载完成后,将立即执行。
  • script defer与属性类似async,这提供了一种替代使用 JS 阻塞解析器的方案,区别在于它会在执行之前等待解析完成。

异步与延迟
图片来源:Growing with the Web

Streams API包含一系列令人兴奋的全新性能提升工具。它们允许 JavaScript 通过可读流接收数据,而无需一次性接收所有数据。Streams API 的应用潜力巨大,可以显著加快初始渲染速度,同时后续数据可以随着时间的推移逐步接收。

流是我们对数据处理方式的思考方式转变的一部分(用户界面应该是流畅的、流驱动的,而不是过于结构化和静态的,尽管这是另一个话题),并且可以应用于帮助提高初始加载性能和持续性能。

流媒体万岁
GIF/视频版权归Jake Archibald所有

第三方脚本

无论你对客户端下载的 JavaScript 代码拥有多大的控制权,一旦页面中添加了第三方脚本,这种控制权就会丧失。常用的第三方脚本包括 Google Tag Manager 和 Facebook Pixel。

神秘

第三方脚本的大小不一,并且会对应用程序的性能产生显著影响。它们通常被认为是大型项目的必需品,但是,在做出决定之前,应该充分评估和考虑它们对性能的影响。

如果使用第三方脚本,最好使用上述属性加载它们,async以免defer干扰页面解析。如果您想了解其他提升第三方脚本性能的方法,请查看这篇文章

捆绑销售

在现代应用中,提升初始加载/下载性能(以及诸如可交互时间等指标)的关键在于打包。打包是一种将代码、资源和依赖项打包成一个或多个包的技术。

这些打包工具可以将多种不同的资源(JS、CSS、HTML、图片等)组合起来,并将它们转换为数量更少、性能更高的包。根据所使用的打包工具,可以对打包过程进行大量的配置,从而生成符合应用程序需求的包。

最初,捆绑下载的主要卖点之一是需要下载的文件数量较少。然而,鉴于所有主流浏览器现在都使用 HTTP/2,这已不再是问题,因为现在可以并行发送数据,而无需通过多路复用使用多个 TCP连接

在现代开发中,打包主要用于将我们编写的优美代码转换为丑陋但性能良好且可执行的代码,以便提供给我们的用户。

  • 大多数现代应用程序都需要先对代码进行转译才能在浏览器中运行。CSS-in-JS/SASS 需要转换为 CSS,JSX(如果使用 React)需要转换为 JS,Svelte 组件需要编译。

捆绑包大小

数据包大小(指总数据包大小,而非单个数据包大小)是评估性能/初始加载时间最可量化的方法之一。这是因为比较数据包大小并评估在特定网络速度下下载该数据量所需的时间非常简单。

BundlePhobia是一个很棒的工具,它可以通过添加 NPM 包来直观地显示(包大小)成本;让您可以更明智地决定添加该包的好处与性能/包成本之间的关系。

捆绑恐惧症

Addy Osmani建议将大于某个特定大小的包拆分50-100kb。采用这种代码拆分方式后,延迟加载的优势将更加明显——它本质上是将某些包/功能的导入延迟到特定触发事件发生之后。代码拆分和延迟加载都可以根据需要进行非常精细的调整,我强烈建议您详细了解这两者,看看它们如何能帮助您的应用。

既然加载时间和软件包大小如此重要,那么究竟该如何减小软件包大小呢?

摇树

摇晃树木
图片来源:《宝可梦 剑/盾》

Tree shaking 的核心在于消除死代码——其目标是只包含应用程序运行所需的代码。Tree shaking 的实现得益于ES2015 模块的静态结构;这意味着应用程序的依赖关系可以通过静态语法确定,无需执行任何代码。同样,使用动态导入会使模块无法进行 Tree shaking。

在下面的示例中,我们sum从. 导入了一个函数math.jsmath.js它还包含其他实用函数,例如squaredivide。但是,由于 JS 打包器可以静态扫描代码以查看使用了哪些导出,因此只有该sum函数会被包含在生产包中。

摇晃树木的动作

不同的打包工具执行 tree shaking 的方式不同。有关 tree shaking 以及 Webpack 如何实现它的更多信息,请参阅此处

向后兼容性/“反编译”

向后兼容性始终是影响软件包大小的重要因素。一般来说,网站需要支持的浏览器和浏览器版本越多,打包后的代码就越大;这是因为新版本的 JavaScript 语法比向后兼容的旧版本更简洁。如果您能够专注于主流浏览器并放弃对 IE 等浏览器的支持,这将显著减小软件包的大小。

untranspiling最近,一种名为“转换”(我不确定这是否已被正式命名)的技术越来越受到关注,它本质上与Babel的功能相反——将旧的ES5JavaScript 代码转换为ES2015新的代码。这可以将大多数库的打包大小减少 20-30%。

Jovi De Croock 创建了一个出色的概念验证 (POC) 应用,展示了现代模块化构建与传统构建在打包大小方面的巨大差异。剧透一下:模块化构建的打包大小比传统构建小了近 50%;如果将这种差异推广到更大的应用程序中,加载时间的提升将是巨大的。

随着语法不断发展并变得越来越简洁,如果你能够发布包含大量语法糖(较少使用 polyfill/向后兼容性支持)的打包代码,这将对最终的打包大小产生积极的影响。

图片

2018 年,图片占网站平均内容/大小的21% ;此后,图片对网站大小的影响急剧增加,如今在现代网络上,图片下载内容的比例已接近惊人的40%。即使是小的图片优化,也能对应用程序的性能产生显著的连锁反应。

页面上指定图像尺寸应决定下载图像的大小,这样可以避免下载不必要的大图像,从而减少文件大小。由于现代设备的像素密度差异很大,常规像素测量方法往往不够精确,因此设备像素比 (DPR) 是确定图像尺寸的首选方法。

HTML 内置了许多图像优化功能,因此无需编写大量复杂的代码手动操作。元素的 srcset 属性允许您指定一组图像尺寸,浏览器会根据当前视口选择合适的尺寸。

有一些渐进式(增强)技术可以实现这一点:首先下载低质量图像,然后随着时间的推移逐步替换为更高质量的版本。这样做的好处在于,页面核心内容能够更快地加载到用户的浏览器中,之后再逐步被更精细、更美观的内容所取代或补充。例如,[gatsby-image]( https://www.gatsbyjs.org/docs/gatsby-image/ )中的 模糊处理技术就是一个很好的例子。如果您决定使用 Gatsby 构建 Web 应用,gatsby-image它还提供了许多其他功能来提升图像性能。

渐进式渲染图像
渐进式图像渲染的示例图

JavaScript 执行

虽然网站的初始加载对性能影响很大,但顾名思义,它主要与会话开始时的性能有关。

为了帮助用户在整个会话期间获得流畅的用户界面体验(尤其是在较大/较复杂的应用程序中),优化 JS 的执行至关重要。

记忆化

简单来说,记忆化本质上是将耗时计算的结果存储在某种缓存中,这样当使用相同的数据/输入再次运行该计算(或函数调用)时,就可以直接返回缓存的结果。一般来说,对会话期间会被多次调用的所有函数进行记忆化通常性能更高(这在组件驱动开发中尤为重要)。

以下代码展示了一个基本的记忆化函数生成器实现。指定的计算函数只有在参数发生变化(或者在本例中参数传递顺序不同)时才会再次运行;否则,将直接从缓存返回值。

记忆化示例

已经有很多文章详细介绍了如何对 JavaScript 进行记忆化;或者,如果您正在使用 UI 库或框架,如何利用可能已公开的记忆化 API。

执行时间

一般来说,大部分繁重的计算工作不应该由客户端 JavaScript 执行。因此,处理速度通常对应用程序的可用性影响甚微。但是,如果客户端必须进行耗时的计算(例如嵌套循环),那么它可能会显著影响 JavaScript 的执行速度,甚至导致程序阻塞。

算法复杂度

算法复杂度通常用一个术语来描述Big O notation
(如果您对此感兴趣,可以阅读Sarah Chima 的这篇文章了解更多信息)。降低算法复杂度可以减少计算负担——即为了获得相同结果而花费的不必要计算/时间。

根据数据量的大小,比较不同的操作方法通常是明智之举。即使每次操作只节省几毫秒,但如果每次操作在会话中重复数百次,也会对用户产生显著的累积影响。Luke Jackson 开发的Perflink是一个分析代码块性能的优秀网站。

代码块性能示例
Perflink对冒泡排序算法与 JS 内置数值排序算法的比较。

我不想过多关注这一部分,因为(至少对于基于 UI 的 JavaScript 而言)几乎没有必要在 JavaScript 浏览器线程中运行无法在其他地方处理的繁重计算任务。

如果你对深入了解 JavaScript 算法感兴趣,那么 Bianca Gandolfo在 Frontend Masters 上有一个关于此主题的精彩演讲。

记忆问题

浏览器现在在执行优化的垃圾回收方面表现出色。这意味着诸如内存泄漏、 未使用的事件监听器等问题很少会造成困扰,因为现代浏览器会在被观察对象变得不可达时自动移除关联的事件处理程序。

虽然内存问题和泄漏的影响通常微乎其微,但了解它们仍然十分重要,因为在某些情况下,它们会导致严重的性能问题。鉴于不同应用程序的内存管理方式差异很大,我认为这超出了本文的讨论范围。如果您想进一步了解内存问题,Kayce Basques 撰写了一篇关于如何识别和修复内存问题的精彩分析文章。

卸货工作

如果我们想让应用程序性能更佳,就应该减少“工作量”……对吧?对于大型应用程序或性能至关重要的应用程序来说,任何可以在客户端脚本运行之前或与之并行完成的工作通常都是明智之举。

Web Workers

利用 Web Worker 可以将脚本运行在后台线程中,从而减轻主线程的压力。虽然 Worker 启动速度可能较慢,但线程间通信速度极快。目前,Web Worker 的应用场景仍然非常有限,尚未得到广泛普及。James Milner 曾撰写过一篇关于 Web Worker 性能的文章,探讨了在哪些情况下需要权衡利弊。

JS 主线程可以生成无限数量的 Web Worker,直到用户资源完全耗尽为止。OffscreenCanvas 就是一个非常适合使用 Web Worker 的例子因为 canvas 逻辑通常计算量很大,最好将其完全从主线程卸载出去。

Chrome 80刚刚新增了对模块工作线程的支持。这意味着工作线程现在可以充分利用 JS 模块的所有优势:动态导入、并行依赖加载、优化执行等等。由于现在很多新的 JS 代码都是以模块的形式编写的,因此工作线程也拥有这些功能就显得尤为重要。

工作区

Worklets本质上是 Web Workers 的轻量级版本,其功能仅限于执行特定功能。

如果您的需求可以通过某个可用的 Worklet 来满足,那么使用其中一个 Worklet 来代替一个完整的 Worker 可能就很有意义了。

Web API 的

虽然前面已经提到了 Web API,但它们也可以用来分担工作。市面上有很多 Web API 可供选择——它们允许浏览器处理任务,而 JavaScript 线程则可以不间断地继续运行。任务完成后,可能会触发一个回调函数,使 JavaScript 线程重新进入执行。

例如,与其在客户端 JS 中编写复杂的自定义逻辑来存储和检索数据,不如与IndexedDB API 交互,并抽象出逻辑和读/写性能,这样可能更有意义。

服务人员

Service Worker 与 Web Worker有些相似之处,它是一种在后台运行、独立于页面的脚本。主要区别在于,Service Worker 被设计为应用程序和网络之间的代理。

由于其核心用途是与网络交互并修改响应,Service Worker 通常与离线应用联系在一起。Service Worker 通过利用Cache API来存储和检索网络请求及其相关响应,从而实现这一目标。

从性能角度来看,设置特定的网络缓存规则,以便在应用程序离线或资源尚不需要重新获取时,能够立即从缓存中返回所需的资源/内容,而无需等待网络响应。

Jake Archibald 的《离线 Cookbook》定义了所有可用于 Service Worker 和 Cache API 的不同缓存规则。例如,资源是否应该始终从缓存返回,或者是否应该优先从网络获取资源,如果网络不可用则回退到缓存。

社会主义共和国

如果你开发的应用依赖于 JavaScript 对 DOM 进行更新,那么 SSR 会对性能和初始加载时间产生显著影响。我不确定应该把这部分内容放在哪个章节,因为它改变了基于 JS 的应用的初始加载和后续执行方式。

服务器端渲染的应用中,客户端会下载预渲染的 HTML,浏览器渲染完成后即可立即查看,无需等待 JavaScript 下载和执行即可浏览内容。这有助于提升诸如最大内容绘制 (LCP)等指标的性能。

服务器端渲染
图片来源:Alex Grigoryan

虽然基于服务器端渲染 (SSR) 的应用在技术上无需 JavaScript 也能“运行”并显示内容,但要实现任何有用的功能,仍然需要 JavaScript。其优势在于,可以在下载或执行 JavaScript 之前渲染并显示 HTML 和内容。

大多数框架和 UI 库都会公开实用函数,以便在服务器端将您的应用程序转换为静态 HTML,然后在客户端对其进行加载。

为了进一步优化,可以将渲染后的 HTML分块传输到浏览器,从而缩短首字节到达时间 (TTFB)。在 React 中,这是通过renderToNodeStream方法实现的。

渲染图

如今,刷新率高达120Hz的设备已经出现。这意味着,为了确保流畅的 UI 体验,渲染至关重要。如果您正在使用组件驱动开发(Component Driven Development),那么本节内容尤其重要,因为在组件驱动开发中,整个组件都会重新渲染,而不是针对单个 DOM 节点进行更改。

在现代(Web 应用)开发中,组件渲染异常(通常渲染次数超出预期)的情况非常普遍。这会对组件树中的子组件产生连锁反应,这意味着如果顶层组件的重新渲染处理不当,可能会导致应用中的每个组件都重新渲染,从而执行大量不必要的渲染逻辑和 DOM 更新。

具体来说,在 React 中,纯组件(或用 `<script>` 包裹的函数组件React.memo)只有在 props 发生变化时才会重新渲染。而在 Hooks 的世界里,诸如 `getResources()`React.useCallback和 ` getResources()` 之类的辅助方法React.useMemo可以自动进行memoization,从而确保渲染项不会因为 props 的变化而改变。可以看看 Andy Richardson 的文章,了解它们的优势。

Why Did You Render是一个非常实用的插件,你可以将其集成到你的 React 应用中,它会提供每个组件渲染的数据,帮助你诊断不必要的渲染。消除不必要的渲染可以释放资源,让开发者专注于真正需要的渲染,从而提升用户体验。

你为什么渲染
图片来源: Welldone Software,作者 why-did-you-render

绩效衡量

你认为你的应用性能已经很好了?太好了!但是,你该如何量化这种性能,并监控改进之处和障碍呢?

性能衡量指标可能非常主观——部分原因在于你衡量的是应用的外部因素,还是最终的用户体验。谷歌汇总了一份以用户为中心的指标清单,他们认为这些指标最为重要。

RAIL 模型由 Chrome 团队于 2015 年提出;他们将其称为以用户为中心的性能模型,该模型将用户体验分解为关键操作。RAIL 的目标全部围绕以用户为中心的指标展开,通过用户对应用的感知来衡量应用。

谷歌铁路模型
图片来源Sven Scheuermeier

绘画

这些指标衡量的是网页内容渲染的速度,也就是用户能够快速浏览网页的速度。谷歌一直是渲染性能指标的主要所有者(就像它管理着网页性能的许多其他方面一样。感谢谷歌!),并推出了许多不同的指标,所有这些指标都以用户体验为中心。所有渲染指标都会报告特定内容渲染并显示给用户的速度。

使用这些指标可以很好地反映用户看到页面重要内容的速度。更快地看到内容(在LCP的情况下,指的是核心内容)将提高用户满意度并降低跳出率。

衡量绩效的工具

灯塔

Lighthouse 是一款优秀的开源工具,可用于直观地了解网站性能——它可以通过 Chrome 开发者工具或 Chrome 扩展程序轻松地对任何网站运行。此外,还可以使用 Google 的PageSpeed Insights运行 Lighthouse 测试的简化版本,该版本支持任何 URL。

谷歌灯塔

首次有效绘制 (FMP) 已被弃用,并且很可能在 Lighthouse 的新版本中被 LCP 取代。

TimeToInteractive是评估网页性能的绝佳指标,因为它衡量网页显示有用内容 (FCP)、注册事件处理程序并及时开始响应用户交互所需的时间。

网页测试

WebPageTest是另一项您可以(免费)用于分析任何给定网站的服务。

虽然结果和指标与 Lighthouse 类似,但它是另一个可以用来深入了解高级性能的优秀工具。

浏览器分析器

所有主流浏览器都提供性能分析器,允许您在用户会话期间记录和分析用户界面 (UI) 的性能和响应速度。这些分析结果非常详细,允许您检查(除其他事项外)执行时间、JS 调用堆栈、绘制指标以及会话期间任何时刻可见的内容。

Google性能分析器

虽然上面的图片乍一看可能令人望而生畏,但了解如何浏览它的基本知识对于调试性能问题真的很有帮助。

我特别想提请您注意一下火焰图(图中色彩鲜艳、看起来像倒置火焰的中间部分)。火焰图本质上是对 JavaScript 调用栈随时间变化的描绘,可以帮助您深入了解哪些代码可能性能不佳或阻塞了线程。

我能提供的一个经验性建议是,理想情况下,火焰图的火焰尖端应该非常细——这意味着虽然调用栈可能很高,但每个函数调用都能快速执行,不会长时间阻塞。如果火焰图的柱状图很粗,表明函数执行缓慢,那么找出罪魁祸首可能是提升性能的一个好切入点。

如果想用不太直观的方式找出运行时间过长的任务,还可以尝试使用实验性的Long Tasks API来识别阻塞主线程50 毫秒或更长时间的任务。

持续监测性能

一旦你了解了当前应用的性能,持续跟踪性能变化就显得尤为重要。这样你就可以逐步改进应用,并将这些改进与应用性能的变化关联起来。例如,如果你的最低可用性能要求 (LCP) 突然大幅上升——最近是否有代码更改导致了性能下降?

通常,你会将性能监控工具连接到客户端 JavaScript 代码中,以便它们可以与应用程序并行运行,并向你选择使用的任何日志记录或数据可视化工具提供数据。Perfume 就是这样一款工具它既是开源的,又可以免费使用。

这个领域有很多同类工具,我估计每个工具都有各自的优缺点。重要的是要根据你的应用需求来评估工具的功能;同时也要记住,由于这些工具通常运行在客户端 JavaScript 上,它们本身可能会对性能产生负面影响(甚至造成性能阻塞)。

结论

希望这能帮助您大致了解为什么集中精力提升应用程序的性能很重要,并概述一些在尝试提高性能时需要牢记的建议和指导原则。

有很多关于如何提高绩效的信息;从小处着手,逐步设定新目标,将有助于你跟踪进度并获得成就感。

文中所有引用的资源均已注明/链接。文中可能混用了英式和美式拼写,敬请谅解。

文章来源:https://dev.to/wgolledge/an-overview-of-performance-in-javascript-applications-2obb