Skip to content

Why do we still need bundlers?

Refer

Source: Why Bundlers - December 26, 2024

Author: rolldown

Translator: SenaoXi

A Note to Our Readers

While maintaining fidelity to the source material, this translation incorporates explanatory content and localized expressions, informed by the translator's domain expertise. These thoughtful additions aim to enhance readers' comprehension of the original text's core messages. For any inquiries about the content, we welcome you to engage in the discussion section or consult the source text for reference.

Skipping the build step is impractical

虽然现代浏览器已经普遍支持原生 es 模块和 http/2,一些开发者提倡在生产环境中采用无打包的方式部署 web 应用。但在我们看来,如果你正在开发任何 非简单 的应用程序,并且关注性能(这直接关系到用户体验),打包 仍然是非常必要的。

即使在一个经过精心设计的无打包部署模式中,构建步骤往往也是不可避免的。以 Rails 8 的默认基于 import map 的方法为例:所有的 javascript 资源仍需经过 构建步骤 来生成指纹签名,收集这些指纹签名作为生成 import mapmodulepreload 指令的数据。这个流程仅通过 importmap-railsPropshaft 处理,而没有通过 捆绑器 处理罢了。

更重要的是,如果你有以下任何需求,无打包方案将会遇到瓶颈:

  • 需要使用现代 javascript 特性,如 es6+typescriptjsx
  • 需要利用 捆绑器 特有的优化,如 tree-shakingcode splittingminification
  • 使用需依赖于构建步骤的库或框架。
  • 使用库作者未打包源代码的 npm 依赖(会导致过多的请求)。

选择无打包意味着将自己限制在 javascript 生态系统的一小部分内,并且放弃许多可能使最终用户受益的性能优化。

反对使用 javascript 打包工具的主要论点是增加了复杂性并降低了开发反馈循环的速度。然而,现代 javascript 工具在这方面已经有了很大改进。vite / rolldown 的目标就是进一步改善这些方面,使构建步骤变得近乎无感知。

The case for bundlers

从根本上说,捆绑器 的存在是因为 web 应用程序的 独特约束依赖资源需要通过网络按需交付

捆绑器 会通过以下三种方式来提高 web 应用程序的性能:

  • 减少网络请求数量和瀑布流。
  • 减少网络传输的总字节数。
  • 提高 javascript 执行性能。

Reduce network requests and waterfalls

首先需要认识到,http/2 的出现并不意味着开发者可以不再关心 HTTP 请求的数量。

尽管 http/2 理论上支持无限的多路复用,但大多数浏览器/服务器对每个连接的最大并发流的默认限制约为 100。每个网络请求都会在服务器和客户端产生固定开销(例如 头部处理TLS 加密、多路复用 等)。更多的请求意味着更高的服务器负载,实际并发性受限于服务器提供模块文件的速度。即使在 http/2 下,包含数千个未打包模块的应用程序仍会造成 严重 的网络瓶颈。

深层级的依赖链 会导致网络瀑布流 — 因为浏览器需要多次网络往返才能获取到应用的整个模块图。虽然可以通过 modulepreload 指令引导浏览器预先加载后续层级所需的资源而在一定程度上得到缓解,但生成这些指令是需要工具支持,而且在 html<head> 中塞入数千个 modulepreload 指令会使 HTML 臃肿不堪,本身也会导致性能问题。

此时,打包 可以通过将数千个模块组合成 服务器浏览器 都能轻松处理的 最优数量的代码块,这 大大减少上述的开销。同时 打包 还可以通过扁平化模块依赖图来 降低依赖链的层级 从而减少瀑布流,并能提供生成 modulepreload 指令所需的数据。

本质上,打包 是将组合模块图的工作转移到 构建阶段,而非像无打包方案那样让每个访问者都承担 运行时成本。这使得大型应用程序在首次访问时加载速度显著提高,尤其是在网络条件不佳的情况下。

Trade-offs in caching strategy

支持无打包方案的一个 论点可以让每个模块单独缓存,减少应用程序更新时的缓存失效。但这需要以上述更慢的初始加载为代价。

打包 配置不理想可能会导致 级联分块哈希验证,从而导致用户在更新应用时必须重新下载应用的很大一部分(chunk hash 失效)。但这是一个可以解决的问题:捆绑器 可以利用 import maps高级分块控制 来限制哈希失效并提高缓存命中率。我们打算在未来在 vite/rolldown 中提供改进过的更有利于缓存的 默认分块策略

Reduce total bytes sent over the network

打包 还可以大大减少通过网络传输的 javascript 字节数。

首先,捆绑器 会将多个模块提升到同一作用域,消除它们之间的所有 import/export 语句。

其次,tree-shaking / 死代码消除 是一种只能在 构建 时通过 静态分析 源代码来执行的优化。原生 esm 加载器会急切地加载和解析所有相关模块,这意味着即使你只使用一个大模块中的单个导出,但对于原生 esm 加载器来说,整个模块也必须被加载并解析,这在某些场景下是 昂贵 的操作。但通过智能 捆绑器,大模块中未使用的导出可以通过 静态分析 完全从最终产物中移除,这大大节省最终 产物的字节数 和提高原生 esm执行效率

最后,与单个模块相比,在打包后的产物上执行 最小化gzip / brotli 压缩 的效率要高得多。

结合上述这些因素,打包操作会让用户下载的代码更少,并且服务器使用的 出站带宽 更少。

Improve JavaScript execution performance

javascript 是一种解释型语言,现代 javascript 引擎通常采用先进的 JIT 编译来提高运行速度。然而,解析和编译 javascript 也会产生不小的成本。

生成更少的 javascript 代码不仅节省带宽,还意味着浏览器需要编译和解析的 javascript 更少,从而缩短应用程序的 启动时间

一些 捆绑器 / 压缩器 还可以在不同程度上执行诸如 常量折叠(constant folding) / 提前解析(ahead-of-time evaluation) 之类的优化,使得打包后的代码比手写源代码更高效。


总而言之,打包Web 开发中仍然是有益的,并且在许多情况下是必要的步骤,并且在可预见的未来仍将如此。

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (2619af4)