精简你的JavaScript:掌握打包优化技巧
在过去15年里,JavaScript生态系统迅速扩张,引入了无数工具,让开发变得更加简单。但这些工具也带来了代价:包体积不断增大。事实上,HTTP Archive的数据表明,每页传输的JavaScript平均量已从2010年的90 KB激增到2024年的650 KB (数据来源)。
尽管采用率在增长并且压缩技术也在进步,这种趋势并没有显示出减缓的迹象。随着我们不断增加功能,挑战依旧:我们如何减少发布的JS代码量?
有趣的是,这些解决方案既简单又复杂。简单之处在于项目级别的调整措施能够快速见效。更难的是产生持久的影响,这需要整个社区共同努力,改进打包工具和库,以及相关工具。
这篇文章聚焦于你可以为项目实施的实际改进措施,包括:
- 打包器:优化构建工具以减少输出大小。
- 库包:明智地选择和使用外部依赖。
- 您的项目:减小包大小的实际步骤。
未来的文章将讨论我们可以进行的生态系统层面的改进,但目前,让我们看看这些因素如何导致臃肿的软件包(臃肿的捆绑包)——以及如何管理这些软件包。
为什么需要优化JavaScript?JavaScript 是现代网页互动的核心动力,但它有成本。JavaScript 是你浏览器需要处理的最耗资源的部分。它常常是决定一个页面是否感觉快速或迟钝的瓶颈,一个臃肿的脚本包会阻塞渲染并拖慢页面速度。
JavaScript包越大,加载时间就越长。这会减慢其他一切,比如显示页面内容或让用户与页面互动。对于有高性能笔记本和光纤宽带连接的人来说,这可能只是一个小麻烦。但对于使用低性能手机或网络状况不佳的人来说,这可能是他们是否继续留在你网站上的决定性因素。
减少JavaScript包大小的第一步是进行代码树摇动(或称为“死代码消除”),大多数打包工具都会自动完成这项任务。但所有的打包工具都一样吗?还是有所不同?
打包器JavaScript 中的打包技术已经走了很长一段路——从手动拼接和任务运行器到现在的高级打包工具。如今,打包工具的性能表现是重点,开发人员更注重快速构建。然而,构建速度并不是唯一的考量。同样重要的是它们生成的包的大小,更小的包意味着用户加载速度更快。
为了追求更好的性能,我们将打包工具从JavaScript改写成了像Rust和Go这样的语言。这需要从头开始重新编写,所以旧的打包工具中的每一个功能和优化都需要重新实现。从长远来看,这样做的努力可能会得到回报。然而,在短期内,这意味着这些新的打包工具缺少了一些经过多年发展才具备的功能,比如JavaScript打包工具中的树摇和压缩。而这些功能正是可以帮助我们最小化打包大小的。
基准点
当然,说说而已,让我们来看看数字吧。
我们来比较八个流行的库,并用七个流行的打包器来打包它们。为了确保公平,我采用了以下方法:
- Node 22.12.0
- 使用
node --run
的平均构建时间 (基于 10 次运行,其中包括 2 次预热运行) - 移除所有的注释(包括许可证相关的注释),因为打包工具处理注释的方式不同
你可以查看基准设置仓库以了解具体的配置详情。
捆绑包测试:
- esbuild (
0.24.0
) 带内置压缩功能 - Parcel (
2.13.2
) 带内置压缩功能 - Rolldown (
0.15.1
) 与rollup-plugin-esbuild
压缩插件一起使用 - Rollup (
4.28.0
) 与rollup-plugin-esbuild
压缩插件一起使用 - Rspack (
1.1.5
) 带内置压缩功能 - Vite (
6.0.3
) 带内置压缩功能 - webpack (
5.97.1
) 与 swc 压缩插件一起使用
注意,本文撰写时,Rolldown 目前仍处于 alpha 阶段,因此它尚不成熟,但其结果很可能会随着时间的推移而变得更好。
测试过的库:
chart.js
(图表库)ckeditor5
(文本编辑器)d3
(数据可视化库)handsontable
(表格编辑库)luxon
(日期和时间处理库)mobx
(状态管理库)tippy.js
(工具提示库)zod
(类型验证库)
这些库在大小和功能上各不相同——有些几乎可以像一个独立的应用程序一样运行。
加快速度
我们先来看看构建速度,因为开发者似乎非常重视这一点。在打包这些库的时候,esbuild 表现最佳,构建时间为 229 毫秒。与最慢的构建时间 7.78 秒相比,它快了 34 多倍。
注意,链接格式在Markdown中通常应该是:
根据这些结果,我们可将打包器分成三大类。
- 闪电般快™:esbuild、Rolldown、启用缓存的Parcel构建(<1秒,esbuild不到250毫秒)。
- 更快的:Rspack(大约2.5秒)。
- 较慢的:未启用缓存的Parcel构建、Vite、Rollup、webpack(每个大约5秒以上)。
差异非常明显,比如。显著地,Rolldown 比 Rollup 快 7 倍,而 Rspack 比 webpack 快 3 倍——同时保持了理论上的向后兼容性。切换到这些更现代的打包工具可以极大地提升大型项目的开发效率。
输出大小
虽然输出大小的差异没那么明显,但还是挺重要的。
汇总结果
将这八个库打包在一起时,Vite 的输出更小,仅 2087 KB,比最大的 2491 KB 小了超过 19.5%。
19.5%的输出大小差异是相当大的:在慢速的3G连接下,最小的包可能需要大约5.7秒来下载,而最大的包则接近7秒下载。解析和执行时间也会随着包大小增加而增加,因此实际差异可能更加明显。
根据这些结果,我们可以将打包器的结果分为三类。
- 最小:Vite,Rollup,esbuild,Parcel 和 Rolldown (~2087–2163千字节)。
- 一般:webpack (~2317千字节)。
- 比较大:Rspack (~2491千字节)。
个人库
汇总的结果并不能完全展现全貌,因为你可能不会用到所有上面提到的库。更有趣的是这些打包器是如何处理这些单独的库的。
对于像 chart.js
这样的库,选择打包器可以显著影响输出大小,输出大小的差异可达 70%。这突显了测试打包器与特定依赖项配合的重要性。在其他大多数情况下,差异要小得多,大约在 35% 左右。
此外,总体来看,webpack 的表现居中,在 8 种情况中,它在其中 5 种情况下表现最好。然而,在打包 handsontable
和 chart.js
时表现较差,最终表现不佳。这表明,根据你选择的库,webpack 可能是个不错的选择。
在另一端,我们有 Rspack。在 8 种情况里最差的 5 种里,它表现最糟糕,生成的包比其他打包工具大很多。
如果你在考虑使用一个打包器,用你常用的库来测试一下,看看是否可以加快构建速度而不增加输出大小?
打包大小 vs. 输出速度
如所示,某些打包器可能会生成比其他打包器更大的打包文件,结果也会随着使用的库的不同而变化。选择打包器时,不要只比较构建时间,还要注意生成的打包文件大小。你可能会发现自己牺牲了更快的构建速度来换取更大的打包文件。
例如,Angular 从 webpack 切换到 esbuild 后,一些开发者报告称空的应用体积增加了大约 20 KB(详情见 此处)。这完美地展示了构建速度和打包大小之间的权衡。
这并不是说你不应该关注构建速度,因为它对开发者的效率和幸福感很重要。此外,CI构建时间通常与合并代码所需的时间有关联。(更详细信息请参阅:此处)
首先,选择捆绑器时要看看它提供的功能。然后力求在打包速度和包大小之间找到平衡。选择可以在你感到舒适的时间内生成尽可能小的包的捆绑器。
测试项目中一些有代表性的库。如果你的依赖项占代码库的大部分,这些基准测试中的差异可能很好地反映你的实际情况。
库接下来我们要讨论的是外部库,这些库往往占了你 JavaScript 包的大部分。在我参与过的项目中,无论是多数还是几乎全部,它们占了包大小的大部分。这就是为什么明智地选择和使用它们如此重要的原因。
金虽然好,但太旧了
我们中的许多人安装了诸如lodash
、axios
或moment
这样的库,只是为了使用它们中的一个函数——这导致了应用程序变得臃肿。这些库非常棒并且在历史上非常重要,但随着它们变得越来越流行,出现了更轻便的替代品,并且其中一些功能被直接添加到了语言本身中。
我们可以利用这一点。我可以列出这些库的原生API或更新、更小的替代品,但是已经有许多文章在介绍这些内容了。还有那么多其他库,根本无法一一涵盖它们。
那就是为什么我只会给你一个通用的建议,看看你用到的库,是否可以移除或替换为原生API或更小的替代品。YOU MIGHT NOT NEED * 这个网站是很好的资源,帮助开始。
更佳的安装位置选择
大多数库默认情况下并没有针对大小进行优化,但有些提供了一些特殊的安装路径或部分构建选项。即使在我们测试的库中,chart.js
、handsontable
和 ckeditor5
允许你通过仅包含你需要的部分来减小大小。我们以 ckeditor5
为例来看看。
默认安装路径下,包体积会在660到785 KiB之间。但如果采用优化路径,包体积会降到603到659 KiB之间。这相当于包体积减少了7%到23%,具体减少比例取决于所用的打包工具。
重复的:依赖项
另一个常见的问题是重复的依赖项。这个问题在JavaScript应用程序中非常常见,例如,Bluesky的嵌入部件同时使用了两个版本的zod
验证库。移除重复的依赖项使打包大小减少了大约9%。
这个问题通常不是因为你同时拉取了同一个库的两个不同版本,而是因为你和其中一个外部库依赖同一个库但版本不一致引起。这通常可以通过升级你所依赖的库来解决。
你的项目考虑到以上几点,我们终于可以来谈谈最后一部分——你的项目部分。这里有一些你可以采取的措施来减小你的包的大小以及提高性能。
检查一下你的包裹。
第一步是让内容变得可见。不了解包内的内容,缩小它们的大小就成了猜谜游戏。你可以用我创建的一个工具叫Sonda,它是一个包分析器和可视化工具。它能和大多数打包工具配合使用(除了Parcel之外),并准确显示出构成包的各个文件的大小。
你可以从在你的项目中安装它,并然后检查包中部分组件的可视化情况。
一旦你对这些包(bundle)的内部结构有了很好的理解,并确定了哪些部分可以优化,你可以点击这些图块查看:
- 压缩前后文件的大小,
- 列出导入所选文件的文件,
- 甚至是查看打包中的源代码部分。
Sonda还会提醒你有关重复依赖的问题,这样你可以快速找到并解决根本问题。
理想情况下,你不仅应该进行一次性的检查,还应该建立持续监控作为你CI管道的一部分。跟踪变化,尤其是在大型项目中,可以帮助你防止小的变化逐渐积累成显著的膨胀。小的变化会像滚雪球一样逐渐积累成显著的膨胀。
移除或精简外部库
最快的代码是你没有发布的那部分。尽可能地做到这一点:
- 移除可以被原生 API 替代的库。
- 用较小的库替换大型库。
- 如果库支持,使用优化后的安装路径。
使用代码分割技术
如果你无法移除应用程序中的某些部分,可以尝试代码分割技术。代码分割技术允许你直到真正需要时才加载这些部分,从而提高初始加载速度。
使用动态 import()
按需加载模块。例如,某个功能可能直到用户点击按钮时才需要,可以等到用户点击按钮时再加载。
现代前端框架原生支持懒加载,使得代码分割集成到你的工作流中更加简单,比以往任何时候都更简单。
按照最佳实践来做
这是一个通用的建议,不过还是值得重复一下。遵循最佳实践,比如:
- 使用最新版本的
target
,以避免代码被不必要的转译或填充。一些填充包可能在现代浏览器中添加大量不必要的代码,但许多环境仍然默认包含了这些填充包。你也可以设置每年更新target
的提醒。 - 定期更新依赖项,因为新版本可能更小、更快。这也可以避免你处理安全漏洞或重复的依赖项。
- 评估现有的或正在考虑添加的每个依赖项。如果无法证明其必要性,不要添加或寻找更小的替代品。
加入 e18e 生态系统社区(e18e,一个生态系统)
如果你对让网页更快或仅仅是学习新知识感兴趣,可以考虑加入Ecosystem Performance社区。我们主要关注三个方向:
- 清理 — 通过移除冗余依赖或用现代替代品替换,使包更简洁。
- 提速 — 提升常用包的性能。
- 升级 — 打造过时包的现代替代品。
我希望这篇文章能说明你可以用更少的代码实现同样的功能。如果不对包的大小进行管理,它们可能会不受控制地增长,但是即使是小的改动也能显著提升表现。
从今天起:分析你的套餐组合,测试一个新工具,或替换一个大体积库。这些改变会让你大吃一惊。
……
希望你喜欢这篇文章。如果有任何疑问或想要讨论,或希望了解更多特定主题的内容,请在下面的评论中告诉我。如果你想了解更多关于 JavaScript 性能、打包和树摇的内容,可以在这里或在 BlueSky 关注我,并加入 e18e 社区。
共同学习,写下你的评论
评论加载中...
作者其他优质文章