Skip to content

为什么我们仍然需要打包器?

¥Why do we still need bundlers?

跳过构建步骤不切实际

¥Skipping the build step is impractical

随着原生 ES 模块和 HTTP/2 在现代浏览器中的普及,一些开发者正在倡导采用非打包的方式来发布 Web 应用,即使在生产环境中也是如此。虽然这种方法适用于较小的应用,但我们认为,如果你要交付任何重要的应用并且注重性能(这意味着更好的用户体验),打包仍然非常必要。

¥With the general availability of native ES modules and HTTP/2 in modern browsers, some developers are advocating for an unbundled approach for shipping web applications, even in production. While this approach works for smaller applications, in our opinion bundling is still very much necessary if you are shipping anything non-trivial and care about performance (which translates to better user experience).

即使在完善的非打包部署模型中,构建步骤仍然常常不可避免。以 Rails 8 默认的基于 import-map 的方法为例:所有 JavaScript 资源仍需经过构建步骤,以便对资源进行指纹识别并生成导入映射和 modulepreload 指令。它只是通过 importmap-rails 和 Propshaft 而不是 JavaScript 打包器来处理。

¥Even in a polished unbundled deployment model, a build step is still often unavoidable. Take Rails 8's default import-map-based approach for example: all JavaScript assets still go through a build step in order to fingerprint the assets and generate the import map and modulepreload directives. It's just handled via importmap-rails and Propshaft instead of a JavaScript bundler.

此外,如果你有以下任何需求,非打包方法将达到其极限:

¥Moreover, the unbundled approach will hit its limits if you have any of the following requirements:

  • 需要现代 JavaScript 功能,例如 ES6+、TypeScript 或 JSX。

    ¥Require modern JavaScript features like ES6+, TypeScript, or JSX.

  • 需要利用打包器特有的优化,例如摇树优化、代码拆分或压缩。

    ¥Need to leverage bundler-specific optimizations like tree-shaking, code splitting, or minification.

  • 使用依赖于构建步骤的库或框架。

    ¥Utilize libraries or frameworks that depend on a build step.

  • 使用 NPM 依赖,这些依赖会以非打包形式提供源代码(会导致请求过多)。

    ¥Utilize NPM dependencies that ship unbundled source code (results in too many requests).

使用非打包方式意味着你将被排除在 JS 生态系统的很大一部分之外,并放弃许多可能有益于终端用户的性能优化。

¥Going with unbundled means locking yourself out of a big part of the JS ecosystem and giving up on many possible performance optimizations that could benefit your end users.

避免使用 JavaScript 打包器的主要原因是它会增加复杂性并减慢开发反馈循环的速度。然而,现代 JS 工具在过去几年中在这方面已经有了很大的改进。Vite/Rolldown 的目标是进一步改进这些方面,让构建步骤变得隐形。

¥The main argument of avoiding JavaScript bundlers is added complexity and slowing down the dev feedback loop. However, modern JS tooling has improved a lot on this front over the past few years. Our goal with Vite / Rolldown is to improve these aspects further and make the build step feel invisible.

打包器案例

¥The case for bundlers

从根本上说,打包器的存在是因为 Web 应用的独特约束:它们需要通过网络按需交付。打包器可以通过三种方式提升 Web 应用的性能:

¥Fundamentally, bundlers exist because of the unique constraints of web applications: they need to be delivered over the network on-demand. Bundlers can make web applications more performant in three ways:

  1. 减少网络请求和瀑布流的数量。

    ¥Reduce the amount of network requests and waterfalls.

  2. 减少通过网络发送的总字节数。

    ¥Reduce total bytes sent over the network.

  3. 提升 JavaScript 执行性能。

    ¥Improve JavaScript execution performance.

减少网络请求和瀑布流

¥Reduce network requests and waterfalls

我们需要承认的第一件重要事情是,HTTP/2 并不意味着你可以不再关心 HTTP 请求的数量。

¥The first important thing we need to acknowledge is that HTTP/2 does not mean you can stop caring about number of HTTP requests.

虽然 HTTP/2 理论上支持无限多路复用,但大多数浏览器/服务器对每个连接的最大并发流数量的默认限制约为 100。每个网络请求还会在服务器和客户端上带来固定的开销(标头处理、TLS 加密、多路复用等)。请求越多,服务器负载就越大,实际并发量取决于服务器处理模块文件的速度。即使在 HTTP/2 下,包含数千个未打包模块的应用仍会造成严重的网络瓶颈。

¥Although HTTP/2 theoretically supports unlimited multiplexing, most browsers / servers have a default limit of around 100 on the maximum number of concurrent streams per connection. Every network request also comes with fixed overhead (header processing, TLS encryption, multiplexing, etc.) on both the server and the client. More requests means more server load, and the actual concurrency is limited by how fast your server can serve the module files. Applications that contain thousands of unbundled modules will still create serious network bottlenecks even under HTTP/2.

深层导入链也会导致网络瀑布 - 也就是说,浏览器需要进行多次网络往返才能获取整个模块图。使用 modulepreload 指令可以在一定程度上缓解这个问题,但生成这些指令需要工具支持,而且在 <head> 中,成千上万的 modulepreload 指令会使 HTML 变得臃肿,这本身也是一个性能问题。

¥Deep import chains also results in network waterfalls - i.e. the browser needs to make multiple network roundtrips to fetch the entire module graph. This can be mitigated to some extent with modulepreload directives, but generating these requires tooling support, and bloating the HTML with thousands of modulepreload directives in <head> is also a performance issue in itself.

打包可以将数千个模块组合成服务器和浏览器都能轻松处理的最佳数量的块,从而大幅减少此类开销。打包还可以扁平化导入链深度以减少瀑布流,并提供生成 modulepreload 指令所需的数据。本质上,打包将模块图的合并工作移至构建阶段,而不是将其作为每个访问者的运行时成本。这使得大型应用在首次访问时加载速度显著加快,尤其是在网络状况不佳的情况下。

¥Bundling can drastically reduce such overhead by combining thousands of modules into an optimal number of chunks that both the server and the browser can handle with ease. Bundling also flattens the import chain depth to reduce waterfalls, and can provide the data needed to generate modulepreload directives. In its essence, bundling moves the work of combining the module graph to the build phase, instead of incurring it as a runtime cost for every visitor. This makes large applications load significantly faster on initial visit, especially in poor network conditions.

缓存策略的权衡

¥Trade-offs in caching strategy

支持非打包方法的一个参数是,它允许每个模块单独缓存,从而减少应用更新时缓存失效的数量。然而,如上所述,这会带来初始加载速度慢得多的代价。

¥One argument supporting the unbundled approach is that it allows each module to be cached individually, reducing the amount of cache invalidation when the application is updated. However, this comes with the trade-off of a much slower initial load as explained above.

不理想的打包配置可能会导致级联的块哈希验证,从而导致用户在应用更新时必须重新下载应用的大部分内容。但这是一个可以解决的问题:打包器还可以利用导入映射和高级分块控制来限制哈希失效并提高缓存命中率。我们计划在未来的 Vite/Rolldown 中提供改进的、更缓存友好的默认分块策略。

¥Sub-optimal bundling configurations can cause cascading chunk hash validations, causing users to have to re-download a large part of the app when the app is updated. But this is a solvable problem: bundlers can also leverage import maps and advanced chunking control to limit hash invalidation and improve cache hit rate. We do intend to provide an improved, more caching-friendly default chunking strategy in Vite / Rolldown in the future.

减少通过网络

¥Reduce total bytes sent over the network

打包还可以显著减少通过网络发送的 JavaScript 的整体大小。

¥Bundling can also greatly reduce overall size of JavaScript sent over the wire.

首先,打包包可以将多个模块提升到同一作用域中,从而删除它们之间的所有 import / export 语句。

¥First, bundles can hoist multiple modules into the same scope, removing all the import / export statements between them.

其次,treeshaking/死代码消除是一种优化,只能通过在构建时静态分析源代码来执行。原生 ESM 会立即加载并执行所有内容,因此即使你只使用大型模块中的单个导出,也必须下载并执行整个模块。使用智能打包器,可以将未使用的导出文件从最终打包包中完全删除,从而节省大量字节。

¥Second, treeshaking / dead code elimination is an optimization that can only be performed by statically analyzing the source code at build time. Native ESM loads and evaluates everything eagerly, so even if you only use a single export from a big module, the entire module has to be downloaded and evaluated. With a smart bundler, exports that are not used can be completely removed from the final bundle, saving lots of bytes.

最后,与单个模块相比,对打包代码执行最小化和 gzip / brotli 压缩的效率要高得多。

¥Finally, minification and gzip / brotli compression are considerably more efficient when performed on bundled code compared to individual modules.

综合考虑这些因素,用户下载的代码更少,服务器使用的出站带宽也更少。

¥With these factors combined, users download less code, and your servers use less outbound bandwidth.

提升 JavaScript 执行性能

¥Improve JavaScript execution performance

JavaScript 是一种解释型语言,现代 JavaScript 引擎通常采用高级 JIT 编译来提高其运行速度。然而,解析和编译 JavaScript 也会产生不小的开销。

¥JavaScript is an interpreted language, and modern JavaScript engines often employ advanced JIT compilation to make it run faster. However, there is also non-trivial cost involved in parsing and compiling JavaScript.

发送更少的 JavaScript 代码不仅可以节省带宽 - 这也意味着需要在浏览器中编译和执行的 JavaScript 更少,从而加快应用启动时间。

¥Sending less JavaScript code not only saves bandwidth - it also means less JavaScript needs to be compiled and evaluated in the browser, leading to faster application startup time.

一些打包器/压缩器还可以不同程度地执行诸如常量折叠/提前求值之类的优化,从而使打包后的代码比手写源代码更高效。

¥Some bundlers / minifiers also can perform optimizations like constant folding / ahead-of-time evaluation to varying extent, making the bundled code more efficient than their hand-written source.


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

¥In conclusion, bundling is still a beneficial, and in many cases necessary step in web development, and will continue to be so in the foreseeable future.