2022 Web 年鉴 — JavaScript

大家好,我是 ConardLi。今天带大家来解读一个比较权威的 JavaScript 年度报告,我们一起来看看今年的 JavaScript 到底是什么样子。

网站加载了多少 JavaScript?

首先,我们评估一下 Web 开发者在 Web 网站上发布的 JavaScript 数量,这相当于是一个环境和背景的评估。

每页加载的 JavaScript 数量

与去年一样,向浏览器发送的 JavaScript 数量又一次增加了。从 2021 年到 2022 年,移动设备增长了 8%,而桌面设备增长了 10%。虽然这种增长没有前几年那么迅速了,但这仍然是一个令人担忧的趋势。虽然我们设备的功能和性能在不断改进,但并不是每个人都在用着最新的设备。事实仍然是更多的 JavaScript 相当于对设备资源造成更大的压力。

未使用的 JavaScript 字节量

根据 Lighthouse 的说法,移动端页面的中位数加载了 162 KB 的未使用 JavaScript。在第 90 个百分位,604 KBJavaScript 未被使用。这比去年略有上升,去年未使用 JavaScript 的中位数和 90% 分别为 155 KB598 KB。所有这些都代表了大量未使用的 JavaScript,而且这里计算的是 JavaScript 资源的传输大小,如果资源被压缩了,则意味着实际已使用 JavaScript 可能比图表显示的要大得多。

与中位数的移动端页面加载的总字节数相比,未使用的 JavaScript 占所有加载脚本的 35%。这比去年的 36% 略有下降,但仍然有很大一部分已加载但未使用。这表明许多页面正在加载可能不会在当前页面上使用的脚本。

每页 JavaScript 请求数

页面上的每个资源都会发送至少一个请求,而且一个资源也有可能对更多资源发出额外请求。

请求越多,你不仅会加载更多的 JavaScript,而且还会增加脚本资源之间的竞争,这可能会阻塞主线程,从而导致启动速度变慢。

每页 JavaScript 请求数

2022 年,移动端页面的中位数响应了 21JavaScript 请求,而在第 90 个百分位,有 60 个。与去年相比,中位数的请求增加了 1 个,第 90 个百分位的请求增加了 4 个。

2022 年的PC端设备而言,中位数有 22JavaScript 请求,第 90 个百分位有 63 个。与去年相比,中位数增加了 1JavaScript 请求,90% 增加了 4 个 — 与移动设备的增长相同。

JavaScript 如何被处理?

自从 Node.jsJavaScript 运行时出现以来,依赖构建工具来打包和编译 JavaScript 变得越来越普遍了。不可否认,这些工具都很有用,但却会影响 JavaScript 的发布数量。

打包

JavaScript 打包工具是一种构建时工具,用于处理项目的 JavaScript 源代码,然后对其进行转换和优化。输出生产环境可使用的 JavaScript。以下面的代码为例:

代码语言:javascript
复制
function sum (a, b) {
  return a + b;
}

打包工具会将这段代码转换为更小更优化的等效代码,从而减少浏览器下载时间:

代码语言:javascript
复制
function n(n,r){return n+r}

鉴于打包工具执行的优化,它们是优化源代码以在生产环境中提升性能的关键手段。

JavaScript 打包工具现在有很多种选择,但我们最经常想到的一个还是 Webpack。幸运的是,Webpack 生成的 JavaScript 包含许多签名,我们可以很方便的检测网站生产环境的 JavaScript 是否使用 Webpack 打包。

使用 webpack 打包 JavaScript 的网站

1000 个最受欢迎的网站中,17% 使用 Webpack 作为打包工具。这也是有道理的,因为网页爬虫抓取的许多热门页面很可能是使用 Webpack 打包和优化源代码的知名电子商务网站。然而,Webpack 并不是唯一使用的打包工具。

使用 Parcel 打包 JavaScript 的网站

ParcelWebpack 的一个不错的替代品,它在所有排名中的使用率都差不多,占排名的 1.2%1.9%

编译

编译器通常在构建时的工具链中使用,它可以将新的 JavaScript 语法转换为可以在旧版浏览器中运行的语法。由于 JavaScript 多年来发展迅速,这些工具被广泛使用中。我们来特殊关注一下在开发者社区中被广泛使用和讨论的 Babel

使用 Babel 的网站

近年来 JavaScript 发展迅速,为了保持对特定浏览器的广泛兼容性,Babel 使用 transforms 来输出兼容的 JavaScript 代码。排名前 100 万的网站中有 26% 正在使用 Babel 转换他们的源代码。

JavaScript 是如何被请求的?

你可以通过多种不同的方式请求 JavaScript,当然不同的请求 JavaScript 的方式也可能对性能产生影响。

async、defer、module、nomodule

<script> 标签的 asyncdefer 属性可以控制脚本的加载方式和行为。async 属性可以异步下载 JavaScript 资源,但会在下载后立即执行,因此仍然可能会阻塞渲染。defer 属性可以延迟脚本的执行,直到 DOM 准备完成,因此可以防止脚本阻塞解析和渲染。

type="module"nomodule 属性是特定于发送到浏览器的 ES6 模块。当使用 type="module" 时,浏览器会期望这些脚本的内容包含 ES6 模块,并将延迟这些脚本的执行,直到默认构建好 DOM。相反的, nomodule 属性会向浏览器表示当前脚本不使用ES6模块。

结果还不错, 76% 的移动端页面会使用 async。但是,defer 的使用率还比较低,这表明仍然有提高渲染性能的空间。

同时使用 asyncdefer 是一种 badcase ,因为 async 的优先级要比 defer 更高一点。

目前 type="module"nomodule 的配置率还被比较低,因为现在很少有页面会提供 ES Module。随着时间的推移,随着开发者直接将未转换的 ES Module 发送到浏览器,type="module" 的使用可能会逐渐增加。

preload、prefetch、modulepreload

这几个属性都是用于浏览器预加载资源的:

  • preload:获取当前导航所需的资源;
  • modulepreload:预加载包含 JavaScript 模块的脚本;
  • prefetch:获取下一次导航所需的资源。

分析资源预加载的使用是比较难的。因为并非所有的页面都可以有优化效果,所以笼统地建议广泛使用资源预加载是不明智的,因为过度使用它们也可能会产生负面的作用。

每页使用 prefetch 的数量

在这里 prefetch 的使用是有点令人惊讶的,每个页面有三个 JavaScript 资源使用了 prefetch。然而,第75百分位和第90百分位的数量表明可能存在相当数量的资源浪费,它们可能是从未发生的页面导航的未使用资源。

每页使用 preload 的数量

在第 90 个百分位看到了有 5JavaScript 资源使用了 preload ,这可能太多了。这表明第 90 个百分位的页面特别依赖 JavaScript,并且正在尝试克服由此产生的性能问题。

<head> 中的 JavaScript

一个古老且经常被吹捧的性能最佳实践是在文档的页脚中加载 JavaScript,以避免脚本的渲染阻塞,并确保在脚本有机会运行之前构建 DOM。然而,近来,在某些架构中将 <script> 元素放置 <head> ,这种行为越来越普遍了。

这可能是在 Web 应用程序中优先加载 JavaScript 的好方法,但是应该尽量使用 async defer 属性以避免 DOM 的渲染阻塞。这样做是为了避免出现一些奇奇怪怪的问题,例如无样式内容的闪烁,或者当 DOM 还没有准备好时,依赖于 DOM 的脚本可能发生 JavaScript 运行时错误。

我们发现 77% 的移动端页面在文档中至少有一个阻塞渲染的脚本,而 79% 的PC页面也存在。这是一个令人担忧的趋势,因为当脚本阻塞渲染时,页面内容的渲染速度会变差。

动态注入的 JavaScript

脚本注入是一种使用 documentJavaScript 中动态创建 HTMLScriptElement 的模式,或者也可以使用 innerHTML 动态插入 <script> 标签。

动态脚本注入是一种相当普遍的做法,它存在的问题是在初始化 HTML 解析是没有办法发现,从而破坏了浏览器的预加载扫描器。

https://web.dev/preload-scanner/#injected-async-scripts

如果注入的脚本资源最终负是责渲染的,这可能会影响例如最大内容绘制 (LCP)之类的指标。

注入脚本的百分比分布

50% 分位中,有 25% 的页面存在动态脚本注入,为了更好的网页性能,我们应该重点评估一下这些动态注入的必要性。

第一方与第三方 JavaScript

我们的网站一般会加载两类 JavaScript

  • 为网站的基本功能提供支持的第一方脚本。
  • 外部供应商提供的满足各种需求的第三方脚本,例如 UX 研究、分析、广告以及嵌入视频和社交媒体功能等内容。

虽然第一方 JavaScript 可能更容易优化,但第三方 JavaScript 本身可能是性能问题的重要来源,因为第三方供应商可能不会在引入新功能的前提下优先考虑优化他们的 JavaScript 资源。

第一方和第三方 JavaScript 请求数量

这个结果值得思考,在各个分位的第一方和第三方脚本的加载数量都差不多,大量的三方脚本会给网页带来较大的性能负担。

动态 import()

动态 import() 是静态 import 语法的一种变体,可以在脚本的任何位置运行,而静态 import 表达式必须在 JavaScript 文件的顶部运行。

https://v8.dev/features/dynamic-import

动态 import() 可以让开发者有效地从他们的主要 JavaScript 包中“拆分”出一些子代码块,以便实现按需加载,这可以通过预加载更少的 JavaScript 来提高启动性能。

目前观察到的所有移动端页面中,只有 0.34% 的页面使用动态 import() 而PC端只有 0.41% ,这一比例低得惊人。这是一个在启动期间交付更少代码的优化手段。

动态 import() 使用起来确实有点难度,但更广泛地采用它可以帮助将加载 JavaScript 的性能成本从页面初始化转移到页面生命周期的后期(网络资源争用较少的时候)。我们希望看到越来越多的网站采用 动态 import()

Web Worker

Web Worker 是一种 Web 平台功能,它通过启动专门的 JavaScript 线程来减少主线程的工作,而无需直接访问自己线程上的 DOM。这项技术可以用来进行卸载任务,否则这些任务可能会通过在单独的线程上完成这项工作来压倒主线程。

https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Using_web_workers

令人欣慰的是,目前有 12% 的移动和PC页面使用一个或多个 Web Worker 来减轻可能使用户体验变差的主要工作线程,但仍有很大的改进空间。

如果你有大量工作可以在不直接访问 DOM 的情况下完成,那么使用 Web Worker 是一个不错的选择。虽然你必须使用专门的通信管道将数据传输到 Web Worker 或从 Web Worker 传输数据,但完全有可能通过使用该技术使你的网页对用户输入的响应更快。

JavaScript 是如何被交付的?

JavaScript 性能的一个同样重要的方面是我们如何向浏览器交付这些脚本,其中包括一些很常见但有时候会错过的优化机会,我们先从压缩 JavaScript 开始。

压缩

压缩是一种常用的优化技术,主要适用于基于一些文本的资产,例如 HTML、CSS、SVG 图像,当然还有 JavaScript。在 web 上有很多广泛使用的压缩技术,可以加快脚本向浏览器的传输速度,有效缩短资源加载阶段的耗时。

JavaScript 压缩方法

有一些压缩技术可以用来减少脚本的传输大小,其中 Brotli (br) 是最有效的方法。虽然 Brotli 的浏览器兼容性现在已经很不错了,但很明显 gzip 还是最受青睐的压缩方法。这可能是由于很多 Web 服务器把它设置为了默认设置。

Brotli:https://github.com/google/brotli

调查结果显示只有 34% 的页面使用 Brotli 压缩脚本,这是一个提高脚本资源的加载性能很明显的机会,但与去年的 30% 相比已经有一些改进了。

未压缩的资源大小

我们可以看到未被压缩的资源主要是一些最小的资源,特别是小于 5KB 的第三方脚本,它们是在没有压缩的情况下交付的。这是因为压缩比较小的资源时可能收益不会那么明显,事实上,动态压缩的额外开销可能会导致一些额外的延迟。但是结果显示仍然有一些机会可以压缩更大的资源,例如一些超过 100KB 的第一方脚本。

缩减

文本资源的缩减也是一种经典的优化方案,例如从代码中删除所有不必要的空格和注释来减少它们的传输大小。uglification 是一种应用于 JavaScript 的进阶做法,它将脚本中的所有变量、类名和函数名简化为更短、不可读的符号。LighthouseMinify JavaScript 审计可以检查未缩减的 JavaScript

未缩减的 JavaScript 分布

0.00 代表最差分数,而 1.00 代表最好分数。在 LighthouseMinify JavaScript 审计中,68% 的移动端页面得分在 0.91.0 之间,而PC页面的得分为 79%。这意味着在移动设备上,32% 的页面有机会进行 JavaScript 缩减优化 ,而PC页面的这一数字为 21%

未缩减 JavaScript 的平均浪费字节数

根据结果显示,未缩减 JavaScript 最大的罪魁祸首是第一方资源,占比略高于 80%,这说明在这个场景下还是有很大的优化机会的。

SourceMap

SourceMapWeb 开发者用来将缩减和丑化的生产代码映射到源代码的一种调试工具。它可以在指向资源末尾的注释中指定,也可以作为 SourceMap HTTP 响应标头指定。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap

通过移动设备访问的 JavaScript 资源中,有 14% 在生产环境保留了 SourceMap 注释,PC 端则有 15%

而使用 SourceMap HTTP 响应标头的移动端页面只有 0.12%

从性能的角度考虑,我们应该尽量避免使用内联源的 SourceMap ,它会将原始源的 base64 映射插入到生产的 JavaScript 资源中。这意味着你不仅将 JavaScript 资源发送给了用户,还额外发送了 SourceMap ,这可能导致 JavaScript 体积过大,需要更长的时间来下载和处理。

JavaScript 的响应能力

JavaScript 影响的不仅仅是网页的启动性能。当我们依赖 JavaScript 实现一些交互的能力时,它们是由需要时间执行的事件处理程序驱动的。根据交互的复杂性和驱动它们所涉及的脚本数量,用户可能会遇到输入响应性能的问题。

长任务/阻塞时间

交互响应能力差的主要原因是执行的任务太长。一般来讲,长任务是在主线程上运行超过 50 毫秒的任何任务。超过 50 毫秒的任务长度就是这个任务的阻塞时间。

长任务是一个需要解决的问题,因为它们会影响主线程执行任何其他工作。当一个页面有很多长任务时,浏览器会觉得响应用户输入很慢。在极端情况下,甚至会感觉浏览器根本没有响应。

每页的长任务数

50% 分位上,移动端页面平均存在 19 个长任务,PC 页面上平均存在 7 个长任务,考虑到PC端一般比移动端具有更强大的处理能力,这个结果还是有点道理的。

仅仅知道长任务的数量还不够,我们再来看一些长任务占用的总时间,移动端页面的中位数是 3.59 秒,而 PC端只有 0.74 秒。

Scheduler API

在以前,浏览器也拥有一些基础的 JavaScript 任务调度能力。例如 requestIdleCallbackand、queueMicrotask,但是这些 API 调度任务的方式比较粗糙,而且使用不当还可能会导致性能问题。

Scheduler API 是一种最新发布的可以根据优先级更好的调度任务的能力。

https://caniuse.com/mdn-api_scheduler_posttask

目前只有 0.002% 的移动端页面使用了 Scheduler API ,PC 端也只有 0.003%。这并不奇怪,这个特性还非常新,文档也比较匮乏,兼容性也比较有限。但是,我们相信随着它的文档的逐步完善,这个数字会增加,尤其是在框架中的使用比率,它是提升更好的用户响应能力的一大利器。

同步 XHR

在以前,XMLHttpRequest(XHR) 是一种非常流行的创建动态用户体验的方法,XHR 有一个标志,允许你发出同步的请求。

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#synchronous_request

同步 XHR 对性能是有损害的,因为在数据返回之前,事件循环和主线程都会被阻塞,导致页面挂起。推荐使用现在已经被浏览器广泛兼容的 fetch 替代。

虽然只有 2.5% 的移动页面和 2.8% 的桌面页面使用了同步 XHR,但仍然表明还有一些应用可能依赖这种损害用户体验的过时方法。

尽量避免使用同步 XHR 和一般的 XHRfetch 是一种更符合人体工程学的替代方案,它在设计上就规避了同步的功能。如果没有同步 XHR,你的页面会表现得更好,我们希望有一天能看到这个数字降到零。

document.write

DOM 插入的方法(例如 appendChild)出现之前,document.write 用于在文档中动态插入内容。document.write 存在比较明显的性能问题,应该尽量避免使用

https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document.write(

现在还有惊人的 18% 的页面仍在使用 document.write 向 DOM 添加内容。这可能是因为一些遗留的应用程序还没有对老的代码进行重写,甚至一些第三方脚本仍在使用它。

所有主流浏览器都明确警告不要使用这种方法。虽然它还没有被弃用,但它在未来几年它应该会被逐步弃用。如果你的网站还在使用 document.write,应优先考虑尽快将其删除。

旧版 JavaScript

JavaScript 在过去几年里有了长足的发展。新语言特性的引入已经将 JavaScript 变成了一种更强大、更优雅的语言,可以帮助开发者编写更简洁的 JavaScript,从而减少 JavaScript 的加载 — 前提是这些特性没有通过使用 Babel 进行不必要的语法转换。

Lighthouse 可以检查一些现代 Web 上可能不需要的 Babel 转换,例如转换使用 async、await、JavaScript 类和其他被广泛支持的语言功能。

超过三分之二的移动端页面正在使用转换的 JavaScript 资源,或者包含不必要的旧版 JavaScript 代码。为了兼容性,语法转换会为生产的 JavaScript 添加大量额外的字节,但除非有必要支持旧版浏览器,否则其中许多转换是不必要的,并且会损害启动性能。

Babel 为解决这个问题做了很多开箱即用的工作,例如 compiler assumptions feature

https://babeljs.io/docs/en/assumptions

Babel 仍然由用户定义的配置驱动,并且只能在存在过时的配置文件的情况下支持。

我们强烈建议开发者仔细检查他们的 BabelBrowserslist 配置,以确保对代码应用最少的转换,在我们目标用户会使用的浏览器版本支持就可以了。这样做会导致发送给最终用户的字节数大大减少。开发者在这方面有很多工作要做,我们希望看到这个数字随着时间的推移而下降,因为 JavaScript 语言的演变已经相对稳定了。

JavaScript 是如何使用的?

Web 开发行业的趋势就是追求抽象,这会使我们的工作更容易~

库和框架

为了了解库和框架的使用,本报告使用了 Wappalyzer 来检测页面上使用的技术:

https://www.wappalyzer.com/

jQuery 仍然是当今网络上使用最多的库,这没什么大惊小怪的。有一部分原因是 35% 的网站使用了 WordPress,但即便如此,有很大一部分项目还在独立使用 jQuery

虽然 jQuery 相对比较小且运行速度相当快,但它仍然带来了一部分额外的性能开销。jQuery 提供的大部分功能现在都可以通过原生 DOM API 实现,其实它在当今的 Web 应用程序开发中可能是不必要的选项了。

core-js 的使用也不足为奇,因为许多 Web 应用程序会使用 Babel 转换代码,Babel 通常会使用 core-js 来填补跨浏览器 API 的兼容性。随着浏览器的逐渐成熟,这个数字应该会下降 — 这确实是一件好事,因为现代浏览器已经越来越强大了,而使用 core-js 代码可能会浪费额外的字节。

与去年相比,React 的使用率明显保持不变,为 8%,这可能表明由于 JavaScript 生态系统中的选择越来越多,它的采用率已趋于平稳。

同时使用的库

在同一页面上同时使用多个框架和库的情况并不少见,很明显,排名靠前的都是 jQuery 生态,core-js 的出镜率也比较高。

安全漏洞

鉴于 JavaScript 在当今 Web 上的广泛传播,JavaScript 生态系统中存在安全漏洞也就不足为奇了。

57% 的移动端页面使用了易受攻击的 JavaScript 库或框架,但这一数字比去年的 64% 有所下降,但要降低这个数字还有很长的路要走。我们希望随着更多的安全漏洞得到修复,开发者也可以逐步提高更新依赖版本的频率,以避免让他们的用户受到伤害。

由于 jQuery 是当今 Web 上最流行的库,它及其相关的 UI 框架代表了当今用户在 Web 上暴露的大量安全漏洞也就不奇怪了。这可能是因为一些开发者仍在使用这些库的旧版本。

另外值得注意的是 Bootstrap,它是一个 UI 框架,可帮助开发者在不直接使用 CSS 的情况下快速构建新布局。鉴于 GridFlexbox 等新兴的 CSS 布局模式的发展,我们可能会看到 Bootstrap 的使用随着时间的推移而减少,或者开发者会更新他们的 Bootstrap 依赖项以发布更安全的网站。

无论你使用什么库和框架,最好的做法都是尽可能的定期更新你的依赖,避免让你的用户受到伤害。虽然包更新确实可能会导致一些重构或奇奇怪怪的问题,但和安全性相比这都是值得的。

最后

  • 报告原文:https://almanac.httparchive.org/en/2022/javascript