为了账号安全,请及时绑定邮箱和手机立即绑定

精简你的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中通常应该是:构建速度基准测试结果

根据这些结果,我们可将打包器分成三大类。

  1. 闪电般快™:esbuild、Rolldown、启用缓存的Parcel构建(<1秒,esbuild不到250毫秒)。
  2. 更快的:Rspack(大约2.5秒)。
  3. 较慢的:未启用缓存的Parcel构建、Vite、Rollup、webpack(每个大约5秒以上)。

差异非常明显,比如。显著地,Rolldown 比 Rollup 快 7 倍,而 Rspack 比 webpack 快 3 倍——同时保持了理论上的向后兼容性。切换到这些更现代的打包工具可以极大地提升大型项目的开发效率。

输出大小

虽然输出大小的差异没那么明显,但还是挺重要的。

汇总结果

将这八个库打包在一起时,Vite 的输出更小,仅 2087 KB,比最大的 2491 KB 小了超过 19.5%。

19.5%的输出大小差异是相当大的:在慢速的3G连接下,最小的包可能需要大约5.7秒来下载,而最大的包则接近7秒下载。解析和执行时间也会随着包大小增加而增加,因此实际差异可能更加明显。

构建大小基准测试的结果

根据这些结果,我们可以将打包器的结果分为三类。

  1. 最小:Vite,Rollup,esbuild,Parcel 和 Rolldown (~2087–2163千字节)。
  2. 一般:webpack (~2317千字节)。
  3. 比较大:Rspack (~2491千字节)。

个人库

汇总的结果并不能完全展现全貌,因为你可能不会用到所有上面提到的库。更有趣的是这些打包器是如何处理这些单独的库的。

各库单独的构建大小基准测试结果

对于像 chart.js 这样的库,选择打包器可以显著影响输出大小,输出大小的差异可达 70%。这突显了测试打包器与特定依赖项配合的重要性。在其他大多数情况下,差异要小得多,大约在 35% 左右。

此外,总体来看,webpack 的表现居中,在 8 种情况中,它在其中 5 种情况下表现最好。然而,在打包 handsontablechart.js 时表现较差,最终表现不佳。这表明,根据你选择的库,webpack 可能是个不错的选择。

在另一端,我们有 Rspack。在 8 种情况里最差的 5 种里,它表现最糟糕,生成的包比其他打包工具大很多。

如果你在考虑使用一个打包器,用你常用的库来测试一下,看看是否可以加快构建速度而不增加输出大小?

打包大小 vs. 输出速度

如所示,某些打包器可能会生成比其他打包器更大的打包文件,结果也会随着使用的库的不同而变化。选择打包器时,不要只比较构建时间,还要注意生成的打包文件大小。你可能会发现自己牺牲了更快的构建速度来换取更大的打包文件。

例如,Angular 从 webpack 切换到 esbuild 后,一些开发者报告称空的应用体积增加了大约 20 KB(详情见 此处)。这完美地展示了构建速度和打包大小之间的权衡。

这并不是说你不应该关注构建速度,因为它对开发者的效率和幸福感很重要。此外,CI构建时间通常与合并代码所需的时间有关联。(更详细信息请参阅:此处)

构建速度和合并时间之间的关系

首先,选择捆绑器时要看看它提供的功能。然后力求在打包速度和包大小之间找到平衡。选择可以在你感到舒适的时间内生成尽可能小的包的捆绑器。

测试项目中一些有代表性的库。如果你的依赖项占代码库的大部分,这些基准测试中的差异可能很好地反映你的实际情况。

接下来我们要讨论的是外部库,这些库往往占了你 JavaScript 包的大部分。在我参与过的项目中,无论是多数还是几乎全部,它们占了包大小的大部分。这就是为什么明智地选择和使用它们如此重要的原因。

金虽然好,但太旧了

我们中的许多人安装了诸如lodashaxiosmoment这样的库,只是为了使用它们中的一个函数——这导致了应用程序变得臃肿。这些库非常棒并且在历史上非常重要,但随着它们变得越来越流行,出现了更轻便的替代品,并且其中一些功能被直接添加到了语言本身中。

我们可以利用这一点。我可以列出这些库的原生API或更新、更小的替代品,但是已经有许多文章在介绍这些内容了。还有那么多其他库,根本无法一一涵盖它们。

那就是为什么我只会给你一个通用的建议,看看你用到的库,是否可以移除或替换为原生API或更小的替代品。YOU MIGHT NOT NEED * 这个网站是很好的资源,帮助开始。

更佳的安装位置选择

大多数库默认情况下并没有针对大小进行优化,但有些提供了一些特殊的安装路径或部分构建选项。即使在我们测试的库中,chart.jshandsontableckeditor5 允许你通过仅包含你需要的部分来减小大小。我们以 ckeditor5 为例来看看。

正常版本和优化版本的对比

默认安装路径下,包体积会在660到785 KiB之间。但如果采用优化路径,包体积会降到603到659 KiB之间。这相当于包体积减少了7%到23%,具体减少比例取决于所用的打包工具。

重复的:依赖项

另一个常见的问题是重复的依赖项。这个问题在JavaScript应用程序中非常常见,例如,Bluesky的嵌入部件同时使用了两个版本的zod验证库。移除重复的依赖项使打包大小减少了大约9%

这个问题通常不是因为你同时拉取了同一个库的两个不同版本,而是因为你和其中一个外部库依赖同一个库但版本不一致引起。这通常可以通过升级你所依赖的库来解决。

你的项目

考虑到以上几点,我们终于可以来谈谈最后一部分——你的项目部分。这里有一些你可以采取的措施来减小你的包的大小以及提高性能。

检查一下你的包裹。

第一步是让内容变得可见。不了解包内的内容,缩小它们的大小就成了猜谜游戏。你可以用我创建的一个工具叫Sonda,它是一个包分析器和可视化工具。它能和大多数打包工具配合使用(除了Parcel之外),并准确显示出构成包的各个文件的大小。

你可以从在你的项目中安装它,并然后检查包中部分组件的可视化情况。

Sonda 项目的树图

一旦你对这些包(bundle)的内部结构有了很好的理解,并确定了哪些部分可以优化,你可以点击这些图块查看:

Sonda还会提醒你有关重复依赖的问题,这样你可以快速找到并解决根本问题。

理想情况下,你不仅应该进行一次性的检查,还应该建立持续监控作为你CI管道的一部分。跟踪变化,尤其是在大型项目中,可以帮助你防止小的变化逐渐积累成显著的膨胀。小的变化会像滚雪球一样逐渐积累成显著的膨胀。

移除或精简外部库

最快的代码是你没有发布的那部分。尽可能地做到这一点:

  • 移除可以被原生 API 替代的库。
  • 用较小的库替换大型库。
  • 如果库支持,使用优化后的安装路径。

使用代码分割技术

如果你无法移除应用程序中的某些部分,可以尝试代码分割技术。代码分割技术允许你直到真正需要时才加载这些部分,从而提高初始加载速度。

使用动态 import() 按需加载模块。例如,某个功能可能直到用户点击按钮时才需要,可以等到用户点击按钮时再加载。

现代前端框架原生支持懒加载,使得代码分割集成到你的工作流中更加简单,比以往任何时候都更简单。

按照最佳实践来做

这是一个通用的建议,不过还是值得重复一下。遵循最佳实践,比如:

  • 使用最新版本的target,以避免代码被不必要的转译或填充。一些填充包可能在现代浏览器中添加大量不必要的代码,但许多环境仍然默认包含了这些填充包。你也可以设置每年更新target的提醒。
  • 定期更新依赖项,因为新版本可能更小、更快。这也可以避免你处理安全漏洞或重复的依赖项。
  • 评估现有的或正在考虑添加的每个依赖项。如果无法证明其必要性,不要添加或寻找更小的替代品。

加入 e18e 生态系统社区(e18e,一个生态系统)

如果你对让网页更快或仅仅是学习新知识感兴趣,可以考虑加入Ecosystem Performance社区。我们主要关注三个方向:

  • 清理 — 通过移除冗余依赖或用现代替代品替换,使包更简洁。
  • 提速 — 提升常用包的性能。
  • 升级 — 打造过时包的现代替代品。
总结

我希望这篇文章能说明你可以用更少的代码实现同样的功能。如果不对包的大小进行管理,它们可能会不受控制地增长,但是即使是小的改动也能显著提升表现。

从今天起:分析你的套餐组合,测试一个新工具,或替换一个大体积库。这些改变会让你大吃一惊。

……

希望你喜欢这篇文章。如果有任何疑问或想要讨论,或希望了解更多特定主题的内容,请在下面的评论中告诉我。如果你想了解更多关于 JavaScript 性能、打包和树摇的内容,可以在这里或在 BlueSky 关注我,并加入 e18e 社区

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消