本文最初发布于 Tim Kadlec 博客,经原作者授权由 InfoQ 中文站翻译并分享。
想要减慢网站的速度,最快的办法就是塞进去一堆 JavaScript 代码了。
JavaScript 的问题是,到最后你要缴纳至少四次性能税:
在网络上下载文件的成本
下载后解析和编译未压缩文件的成本
执行 JavaScript 的成本
内存成本
这种组合非常昂贵:
https://v8.dev/blog/cost-of-javascript-2019
而且我们用的 JS 代码数量越来越多。随着组织纷纷转向由 React、Vue.js 等类似框架驱动的网站,我们网站的核心功能越来越依赖 JavaScript。
我看到很多非常笨重的网站都在使用它们,但由于与我合作的公司之所以来找我,恰恰是因为他们正面临着性能挑战,所以我的观点是带有很大偏见的。我很好奇这种情况有多普遍,以及当我们将这些框架作为默认起点时要付出多少代价。
感谢 HTTP Archive,有了它我们就可以搞清楚这一点了。
数据
HTTP Archive 总共跟踪 4,308,655 个桌面网址和 5,484,239 个移动网址。在 HTTP Archive 针对这些网址报告的诸多数据点中,有一个针对给定站点的检测到的技术列表。这意味着我们可以找出几千个使用各种框架的网站,并查看它们所交付的代码量及 CPU 的成本消耗。
我查询了 2020 年 3 月(撰文时的最近一期)这一期的所有数据。
我决定将 HTTP Archive 记录的所有站点的汇总数据与检测到 React、Vue.js 和 Angular 的站点进行比较。[注 1]
为了加点乐趣,我还添加了 jQuery(它仍然非常流行)。与 React、Vue.js 和 Angular 提供的单页应用程序(SPA)方法相比,它还代表了另一种使用 JavaScript 构建的方式。
HTTP Archive 中检测到的具有特定框架的网站
框架:jQuery
移动网站:4,615,474
桌面网站:3,714,643
框架:React
移动网站:489,827
桌面网站:241,023
框架:Vue.js
移动网站:85,649
桌面网站:43,691
框架:Angular
移动网站:19,423
桌面网址:18,088
希望和梦想
在我们深入研究之前,先说一下我的期望。
在理想的世界中,我认为框架应该超越开发人员的经验价值,并为使用我们网站的人们提供具体的价值。性能只是其中的一部分——可访问性和安全性也都是应该考虑的——但性能是很关键的部分。
因此在理想的世界中,框架可以提供更好的起点或提供一些限制条件和特征,让开发人员难以构建性能不佳的事物,从而帮助开发人员更容易地实现良好的性能。
最好的框架可以兼顾上面两点:提供更好的起点,并帮助开发人员限制事物,防止失控。
只查看我们数据的中位数的话是看不出来这些的,实际上这种方式会遗漏大量信息:
https://timkadlec.com/remembers/2018-06-07-prioritizing-the-long-tail-of-performance/
所以对于每组统计数据,我会提取以下百分点:第 10、25、50(中位数),75 和 90。
第 10 和第 90 个百分点对我来说特别有趣。对于给定的框架,第 10 个百分点代表同类最佳(或至少可以说是接近同类最佳)。换句话说,在使用给定框架的所有站点中,只有 10%达到或超过这一门槛。另一方面,第 90 个百分点恰恰相反:它向我们展示了情况能变得有多糟糕。第 90 个百分点代表长尾,也就是体积最大或主线程耗时最多的最后 10%的站点。
JavaScript 字节数
首先,应该检查的是通过网络传递的 JavaScript 字节数(体积)。10% 的百分点代表数据集的数据由好到差排序时,排在第 10% 的站点数据,以此类推。
向移动设备传输的 JavaScript 体积,按百分点排序
数据集:所有站点
传输大小,10%:93.4kb
传输大小,25%:196.6kb
传输大小,50%:413.5kb
传输大小,75%:746.8kb
传输大小,90%:1,201.6kb
数据集:使用 jQuery 的站点
传输大小,10%:110.3kb
传输大小,25%:219.8kb
传输大小,50%:430.4kb
传输大小,75%:748.6kb
传输大小,90%:1,162.3kb
数据集:使用 Vue.js 的站点
传输大小,10%:244.7kb
传输大小,25%:409.3kb
传输大小,50%:692.1kb
传输大小,75%:1,065.5kb
传输大小,90%:1,570.7kb
数据集:使用 Angular 的站点
传输大小,10%:445.1kb
传输大小,25%:675.6kb
传输大小,50%:1,066.4kb
传输大小,75%:1,761.5kb
传输大小,90%:2,893.2kb
数据集:使用 React 的站点
传输大小,10%:345.8kb
传输大小,25%:441.6kb
传输大小,50%:690.3kb
传输大小,75%:1,238.5kb
传输大小,90%:1,893.6kb
向桌面设备传输的 JavaScript 体积,按百分点排序
数据集:所有站点
传输大小,10%:105.5kb
传输大小,25%:226.6kb
传输大小,50%:450.4kb
传输大小,75%:808.8kb
传输大小,90%:1,267.3kb
数据集:使用 jQuery 的站点
传输大小,10%:121.7kb
传输大小,25%:242.2kb
传输大小,50%:458.3kb
传输大小,75%:803.4kb
传输大小,90%:1,235.3kb
数据集:使用 Vue.js 的站点
传输大小,10%:248.0kb
传输大小,25%:420.1kb
传输大小,50%:718.0kb
传输大小,75%:1,122.5kb
传输大小,90%:1,643.1kb
数据集:使用 Angular 的站点
传输大小,10%:468.8kb
传输大小,25%:716.9kb
传输大小,50%:1,144.2kb
传输大小,75%:1,930.0kb
传输大小,90%:3,283.1kb
数据集:使用 React 的站点
传输大小,10%:308.6kb
传输大小,25%:469.0kb
传输大小,50%:841.9kb
传输大小,75%:1,472.2kb
传输大小,90%:2,197.8kb
站点负载大小数据来看,第 10 个百分点的结果和你想的基本差不多:如果站点使用了这些框架(不管是哪一种),即使在最理想的情况下也会有更多的 JavaScript。这没什么好奇怪的——你不能一边把 JavaScript 框架用作默认起点,另一边还期望框架能让网站使用的 JavaScript 代码变少。
值得注意的是,某些框架比其他对手有着更好的起点。使用 jQuery 的网站表现最佳,其在桌面设备上增加的 JavaScript 约 15%起步,在移动设备上增加的 JavaScript 约 18%起步。(这里确实存在一些偏见。在许多站点上都可以找到 jQuery,因此自然而然地,它与数据整体的关系要比其他框架更为紧密。但是,这并没有改变各个框架统计出来的原始数据所展示的情况。)
值得注意的是,尽管 jQuery 带来了 15-18%的体积增加,但与另一端的数据对比时就会发现 jQuery 税是非常低的。使用 Angular 的网站在桌面上第 10 个百分点的体积膨胀了 344%,在移动设备上则是 377%。第二笨重的 React 网站在桌面上的 JavaScript 体积比总体值高出 193%,在移动设备上则是 270%。
我在前面提到过,就算框架的起点差一些,我也希望它们可以通过某种方式限制代码的体积上限来提供价值。
有趣的是,jQuery 驱动的网站就遵循这种模式。尽管它们在第 10 个百分点上的表现稍微偏高(比总体值高出 15-18%),但在第 90 个百分点上的数据就小得多——桌面和移动版均约为 3%。这些数字都没那么大,所以至少就 jQuery 发送的 JavaScript 代码体积而言,数据的长尾部分似乎没那么糟糕。
其他框架就不是这么一回事了
在第 10 个百分点上,Angular 和 React 驱动的站点比其他框架在第 90 个百分点上的体积还大,令人不爽。
在第 90 个百分点上,Angular 站点在移动设备上发送的字节数增加了 141%,在桌面上发送的字节增加了 159%。使用 React 的网站在桌面上的字节数增加了 73%,在移动设备上的字节数增加了 58%。React 站点在移动设备的第 90 个百分点为 2,197.8kb,比第二差的 Vue.js 多了 322.9kb 的 JavaScript。Angular 和 React 在桌面上与其他框架的差距更大,与 Vue.js 驱动的网站相比,React 驱动的网站提供的 JavaScript 多了 554.7kb。
JavaScript 主线程时间
从数据中可以明显看出,采用这些框架的网站在站点体积方面往往会付出巨大的代价。但当然,这只是等式的一部分。
一旦 JavaScript 代码到达客户端就必须开始执行。浏览器主线程上发生的任何事情都特别麻烦。主线程负责在样式计算、布局和绘制期间处理用户输入。如果我们用大量的 JavaScript 代码来阻塞主线程,那么它就没有机会及时执行这些操作,从而导致卡顿和混乱。
HTTP Archive 记录了 V8 主线程的执行时间,因此我们可以查询这些数据以了解它在所有 JavaScript 代码上的处理时间。
移动设备上脚本相关的 CPU 时间(以毫秒为单位),按百分点排序
数据集:所有网站
主线程时间,10%:356.4 毫秒
主线程时间,25%:959.7 毫秒
主线程时间,50%:2,372.1 毫秒
主线程时间,75%:5,367.3 毫秒
主线程时间,90%:10,485.8 毫秒
数据集:使用 jQuery 的网站
主线程时间,10%:575.3 毫秒
主线程时间,25%:1,147.4 毫秒
主线程时间,50%:2,555.9 毫秒
主线程时间,75%:5,511.0 毫秒
主线程时间,90%:10,349.4 毫秒
数据集:使用 Vue.js 的网站
主线程时间,10%:1,130.0 毫秒
主线程时间,25%:2,087.9 毫秒
主线程时间,50%:4,100.4 毫秒
主线程时间,75%:7,676.1 毫秒
主线程时间,90%:12,849.4 毫秒
数据集:使用 Angular 的网站
主线程时间,10%:1,471.3 毫秒
主线程时间,25%:2,380.1 毫秒
主线程时间,50%:4,118.6 毫秒
主线程时间,75%:7,450.8 毫秒
主线程时间,90%:13,296.4 毫秒
数据集:使用 React 的网站
主线程时间,10%:2,700.1 毫秒
主线程时间,25%:5,090.3 毫秒
主线程时间,50%:9,287.6 毫秒
主线程时间,75%:14,509.6 毫秒
主线程时间,90%:20,813.3 毫秒
桌面设备上脚本相关的 CPU 时间(以毫秒为单位),按百分点排序
数据集:所有网站
主线程时间,10%:146.0 毫秒
主线程时间,25%:351.9 毫秒
主线程时间,50%:831.0 毫秒
主线程时间,75%:1,739.8 毫秒
主线程时间,90%:3,236.8 毫秒
数据集:使用 jQuery 的网站
主线程时间,10%:199.6 毫秒
主线程时间,25%:399.2 毫秒
主线程时间,50%:877.5 毫秒
主线程时间,75%:1,779.9 毫秒
主线程时间,90%:3,215.5 毫秒
数据集:使用 Vue.js 的网站
主线程时间,10%:350.4 毫秒
主线程时间,25%:650.8 毫秒
主线程时间,50%:1,280.7 毫秒
主线程时间,75%:2,388.5 毫秒
主线程时间,90%:4,010.8 毫秒
数据集:使用 Angular 的网站
主线程时间,10%:482.2 毫秒
主线程时间,25%:777.9 毫秒
主线程时间,50%:1,365.5 毫秒
主线程时间,75%:2,400.6 毫秒
主线程时间,90%:4,171.8 毫秒
数据集:使用 React 的网站
主线程时间,10%:508.0 毫秒
主线程时间,25%:1,045.6 毫秒
主线程时间,50%:2,121.1 毫秒
主线程时间,75%:4,235.1 毫秒
主线程时间,90%:7,444.3 毫秒
这里的结果也和前面有很多相似之处。
首先,检测到使用了 jQuery 的网站的 JavaScript 花费在主线程上的时间要比其他三种框架少得多。在第 10 个百分点,在移动设备上的 JavaScript 主线程工作量(相比总体数据)增加了 61%,在桌面设备上则增加了 37%。在第 90 个百分点上,jQuery 网站的表现非常接近总体数据,移动设备的主线程时间 减少 了 1.3%,桌面设备的时间 减少 了 0.7%。
另一头(花费在主线程上的时间最多的框架)又是 Angular 和 React。唯一的区别是,尽管 Angular 站点比 React 站点交付的 JavaScript 代码体积更大,但实际上前者在 CPU 上花费的时间更少,而且是少很多。
在第 10 个百分点,Angular 网站在桌面设备 CPU 上花费的时间增加了 230%,而在移动设备上则增加了 313%。React 网站带来了更多的麻烦,在桌面设备上多出 248%的时间,在移动设备上多出 658%的时间。不,658%不是笔误。在第 10 个百分点,使用 React 框架的网站需要在主线程上花费足足 2.7 秒才能处理完所有服务器发来的的 JavaScript 代码。
与这些吓人的数字相比,第 90 个百分点的情况看起来稍好一些。Angular 网站的主线程在桌面设备上花费的时间增加了 29%,在移动设备上的时间增加了 27%。React 网站在桌面上的时间增加了 130%,在移动设备上的时间增加了 98%。
这些百分比看上去比第 10 个百分点要好得多,但是请记住,绝对数值简直吓死人了:第 90 个百分点上,在移动设备上使用 React 框架构建的网站的主线程工作时间为 20.8s。(我认为那段时间发生的事情足以再写一篇文章了。)
有一个潜在的陷阱(感谢 Jeremy 的建议,让我从这个角度仔细检查了统计数据)——许多站点都会引入多个库。特别是在迁移到新架构时,我看到许多网站都同时使用 jQuery 与 React 或 Vue.js。因此我重新跑了一遍查询,只是这次查的是只包含 React/jQuery/Angular 或 Vue.js,而不是它们的某种组合的站点。
移动设备上脚本相关的 CPU 时间(以毫秒为单位),只检测到一个框架,按百分点排序
数据集:只使用 jQuery 的网站
主线程时间,10%:542.o 毫秒
主线程时间,25%:1,062.2 毫秒
主线程时间,50%:2,297.4 毫秒
主线程时间,75%:4,769.7 毫秒
主线程时间,90%:8,718.2 毫秒
数据集:只使用 Vue.js 的网站
主线程时间,10%:944.0 毫秒
主线程时间,25%:1,716.3 毫秒
主线程时间,50%:3,194.7 毫秒
主线程时间,75%:5,959.6 毫秒
主线程时间,90%:9,843.8 毫秒
数据集:只使用 Angular 的网站
主线程时间,10%:1,328.9 毫秒
主线程时间,25%:2,151.9 毫秒
主线程时间,50%:3,695.3 毫秒
主线程时间,75%:6,629.3 毫秒
主线程时间,90%:11,607.7 毫秒
数据集:只使用 React 的网站
主线程时间,10%:2,443.2 毫秒
主线程时间,25%:4,620.5 毫秒
主线程时间,50%:10,061.4 毫秒
主线程时间,75%:17,074.3 毫秒
主线程时间,90%:24,956.3 毫秒
首先,结果符合预期:当只使用一个框架时,网站的性能(相比多框架并用)往往会明显提升。每个框架在第 10 个百分点和第 25 个百分点时表现更佳。这就说得通了。用一个框架构建好的网站应该比用两个或更多框架构建的网站更快一些。
实际上,除了一个奇怪的例外,每个框架在各个百分点上的成绩都比上一节更好了。令我感到惊讶的是,只使用 React 框架的网站在第 50 个百分点及以后的表现竟然倒退了,这也是我加入这部分数据的原因。
这个现象就有点奇怪了,下面是我的合理猜想。
如果你同时使用 React 和 jQuery,那么很可能是因为你正在迁移到 React 或混合代码库中。既然我们已经看到,使用 jQuery 的网站在主线程上花费的时间比使用 React 的网站更少,所以可以推理,仍由 jQuery 驱动的某些功能会提升整个站点的速度表现。
但是,当你离开 jQuery 而将更多代码迁移到 React 上时,情况就会逐渐发生变化了。如果网站建设得非常好,并且你很少使用 React,那倒没什么问题。但是对于常见的站点来说,内部更多的代码跑在 React 上,意味着主线程承受的压力也随着迁移进程的推进而越来越大。
移动 / 桌面差距
另一个值得关注的角度是,移动体验和桌面体验之间的差距到底有多大。从站点体积的角度来看,两者之间的差异没那么可怕。当然,我希望网站传输的数据能尽量少一些,但是移动设备和桌面设备收到的数据量并没有比对方高出很多。
但一旦转头来看处理时间的话,它们的差距就很惊人了。
与桌面设备相比,移动设备上主线程脚本工作时间增加的百分比,按百分点排序
数据集:所有网站
从桌面到移动设备增加的百分比,10%:144.1%
从桌面到移动设备增加的百分比,25%:172.8%
从桌面到移动设备增加的百分比,50%:185.5%
从桌面到移动设备增加的百分比,75%:208.5%
从桌面到移动设备增加的百分比,90%:224.0%
数据集:使用 jQuery 的网站
从桌面到移动设备增加的百分比,10%:188.2%
从桌面到移动设备增加的百分比,25%:187.4%
从桌面到移动设备增加的百分比,50%:191.3%
从桌面到移动设备增加的百分比,75%:209.6%
从桌面到移动设备增加的百分比,90%:221.9%
数据集:使用 Vue.js 的网站
从桌面到移动设备增加的百分比,10%:222.5%
从桌面到移动设备增加的百分比,25%:220.8%
从桌面到移动设备增加的百分比,50%:220.2%
从桌面到移动设备增加的百分比,75%:221.4%
从桌面到移动设备增加的百分比,90%:220.4%
数据集:使用 Angular 的网站
从桌面到移动设备增加的百分比,10%:205.1%
从桌面到移动设备增加的百分比,25%:206.0%
从桌面到移动设备增加的百分比,50%:201.6%
从桌面到移动设备增加的百分比,75%:210.4%
从桌面到移动设备增加的百分比,90%:218.7%
数据集:使用 React 的网站
从桌面到移动设备增加的百分比,10%:431.5%
从桌面到移动设备增加的百分比,25%:386.8%
从桌面到移动设备增加的百分比,50%:337.9%
从桌面到移动设备增加的百分比,75%:242.6%
从桌面到移动设备增加的百分比,90%:179.6%
其实开始研究前我也想到了手机和笔记本电脑之间会有一些速度差异,但结果如此高的数字告诉我,现在这些流行的框架并没有照顾那些性能较弱的设备,自然也就没法缩减不同性能水平设备之间的差距。即使在第 10 个百分点上,React 站点在移动设备上的主线程工作时间也要比在台式设备上多出 431.5%。jQuery 是所有框架中两个平台差距最小的,但即使是它,在移动设备上的时间也要多出 188.2%。随着我们给 CPU 带来愈加沉重的负担时,那些性能较弱的设备最终会不堪重负。
整体评价
好的框架应该在一些关键要素(安全性、可访问性、性能)上提供一个更好的起点,或者具有内置的约束条件,让开发人员更难做出违反这些规则的代码。
可是在性能要素上这种事情似乎并没有出现(显然可访问性也是一样)。
值得注意的是,就算使用 React 或 Angular 的网站在 CPU 上花费的时间比其他网站更多,也并不代表 React 的 CPU 开销比 Vue.js 更高。实际上,上面这些数据与核心框架的实际性能并没有太大关系,而是更多地反映了在这些框架上开发网站的方法的性能表现,而这些方法是由框架的文档、生态系统和通行编码实践等内容形成的。
还需要注意的是我们这里没有提到的内容:设备在查看后续数据时,在这些 JS 代码上花费的处理时间。SPA 架构的理念是,一旦 SPA 加载完毕,理论上你将获得更快的后续页面加载速度。我自己的经验告诉我实际情况和理想相差甚远,但我们这里没有具体的数据来验证。
显而易见的是:目前,如果你使用某种框架来构建网站,那么即使在最理想的情况下也要牺牲初始加载的速度。
在适当的情况下可以做一些权衡取舍,但重要的是我们必须有意识地做出这种决策。
当然我们也有理由对未来的发展报以乐观的心态。Chrome 团队正在与其中一些框架紧密合作,以帮助改善后者的性能表现,这让我感到很欣慰。
但我也是很务实的。新的架构往往会在解决旧的性能问题的同时频繁地产生新的性能问题,并且纠正问题是要花费时间的。正如我们不应该期望新的高速网络能解决我们所有的性能问题一样,我们也不应该期望我们喜欢的框架的下一个版本能够解决所有这些问题。
如果要使用其中某个框架,开发人员必须采取额外的步骤以确保不会对网站的性能产生负面影响。以下是一些不错的注意事项参考:
做一个健全性检查:你真的需要使用它吗?原版 JavaScript 现在就可以做很多事情。
有没有更轻巧的替代品(Preact,Svelte 等),可以让你达成 90% 的目标?
如果你要使用一个框架,是否有东西可以提供更好、更合理的默认项(例如 Nuxt.js 代替 Vue.js,Next.js 代替 React 等)?[注 2]
你的 JavaScript 的性能预算是多少?
你会在工作流程中引入怎样的约束,避免自己添加那些并非绝对必要的 JavaScript?
如果你要使用的框架是为提升开发效率准备的,那么是否需要将其交付给客户端,还是可以在服务器上处理所有工作?
无论你选择哪种技术,这些都是值得仔细思考的问题;如果你从一开始就遇到了性能缺陷,那么这些问题尤其需要好好考虑了。
附注
1、我考虑过使用其他基准。
例如,我们可以使用未检测到上述任何框架(jQuery、React、Vue.js、Angular)的所有站点。
以下是这些网站在移动设备上运行 JavaScript 的主线程耗时:
移动设备上与脚本相关的 CPU 处理时间(以毫秒为单位),按百分点排序
数据集:所有网站
主线程时间,10%:6.3 毫秒
主线程时间,25%:75.3 毫秒
主线程时间,50%:382.2 毫秒
主线程时间,75%:2,316.6 毫秒
主线程时间,90%:5,504.7 毫秒
更进一步,我们可以使用所有未检测到 JavaScript 框架或库的网站做基准。这些网站的主线程时间如下所示:
移动设备上与脚本相关的 CPU 处理时间(以毫秒为单位),按百分点排序
数据集:所有网站
主线程时间,10%:3.6 毫秒
主线程时间,25%:29.9 毫秒
主线程时间,50%:193.2 毫秒
主线程时间,75%:1,399.6 毫秒
主线程时间,90%:3,714.5ms
无论采用哪种基线,数据看起来都对框架不太有利,而且差别更明显了。最后我选择了聚合数据作为基准,因为:
社区中讨论这种话题时基本都会使用它。
它避免了"没有框架的站点是否足够复杂,以提供准确的对比"的辩论。你完全可以在不使用框架的前提下构建大型复杂站点(我就和这样干的公司合作过),但这个论点会让人忽视数据中本来非常清晰的结论。
不过,很多人都说上面这些基准很有趣,因为它们展示出了整体水平受到了这些工具的影响到底有多大。
2、我怀疑人们只从框架开始,就可以构建出优于 Next.js 或 Nuxt.js 的东西。不过,这里的数据长尾表明,对于许多网站而言情况并非如此。建立起这些框架提供的所有管道,并构建表现良好的代码需要花费很多时间。所以做出引入框架的决策之前,还是应该好好考虑一下自己到底要加入多少抽象层。
英文原文
The Cost of JavaScript Frameworks
评论 5 条评论