写点什么

新一代构建工具 (1):对比 rollup/parcel/esbuild—esbuild 脱颖而出

作者:zhoulujun
  • 2022 年 6 月 25 日
  • 本文字数:8124 字

    阅读完需:约 27 分钟

文章内容来源:

字节前端是如何基于 ESBuild 的做现代化打包设计? https://mp.weixin.qq.com/s/bS_qwiOIMqFN1sfuPKTUbA

新世代建置工具解析(esbuild、Snowpack、Vite、wmr) https://andyyou.github.io/2021/04/25/new-generation-of-build-tools-comparsing/

Esbuild 为什么那么快 Esbuild 为什么那么快 https://zhuanlan.zhihu.com/p/379164359

「 不懂就问 」esbuild 为什么这么快? https://cloud.tencent.com/developer/article/1832345

三大前端构建工具横评,谁是性能之王! https://cloud.tencent.com/developer/article/1806829

深入对比 Webpack、Parcel、Rollup 打包工具 https://zhuanlan.zhihu.com/p/350601275

如何评价 0 配置的 web 打包器 parcel? - 陈成的回答 - 知乎 https://www.zhihu.com/question/263676981/answer/272172727

如何评价 0 配置的 web 打包器 parcel? - Roscoe 的回答 - 知乎https://www.zhihu.com/question/263676981/answer/272288889

webpack 之外的打包工具(Rollup,Parcel) https://juejin.cn/post/6959755835354382367

webpack 或 esbuild:为什么不是两者兼而有之? https://xie.infoq.cn/article/d9c4ca69e0de8fecf176dfd20

esbuild 为什么不用 Rust,而使用了 Go? www.shouhuola.com/article-53417.html

 

什么是 bundler

bundler 的工作就是将一系列通过模块方式组织的代码将其打包成一个或多个文件,我们常见的 bundler 包括 webpack、rollup、esbuild 等。 

  • webpack :强调对 web 开发的支持,尤其是内置了 HMR 的支持,插件系统比较强大,对各种模块系统兼容性最佳(amd,cjs,umd,esm 等,兼容性好的有点过分了,这实际上有利有弊,导致面向 webpack 编程),有丰富的生态,缺点是产物不够干净,产物不支持生成 esm 格式, 插件开发上手较难,不太适合库的开发。

  • rollup: 强调对库开发的支持,基于 ESM 模块系统,对 tree shaking 有着良好的支持,产物非常干净,支持多种输出格式,适合做库的开发,插件 api 比较友好,缺点是对 cjs 支持需要依赖插件,且支持效果不佳需要较多的 hack,不支持 HMR,做应用开发时需要依赖各种插件。

  • parcel:强调极速零配置 Web 应用打包工具,它利用多核处理提供了极快的速度,并且不需要任何配置。

  • esbuild: 强调性能,内置了对 css、图片、react、typescript 等内置支持,编译速度特别快(是 webpack 和 rollup 速度的 100 倍+),缺点是目前插件系统较为简单,生态不如 webpack 和 rollup 成熟。

esbuild vs parcel vs rollup vs snowpack vs webpack 过去一年情形


esbuild vs grunt vs gulp vs parcel vs rollup vs snowpack vs webpack


rollup

rollup 就是一个非常优秀的 bundler,rollup 有着很多非常优良的性质

  • treeshaking 支持非常好,也支持 cjs 的 tree shaking

  • 丰富的插件 hooks,具有非常灵活定制的能力

  • 支持运行在浏览器上

  • 支持多种输出格式(esm,cjs,umd,systemjs)

正式因为上述优良的特性,所以很多最新的 bundler|bundleness 工具都是基于 rollup 或者兼容 rollup 的插件体系,典型的就是 vite 和 wmr

rollup 写插件比起给 webpack 写插件要舒服很多

rollup vs  webpack

如何用 Webpack 和 Rollup 进行比较的话

  • webpack 的优势在于他更加全面,基于”一切皆模块“的思想而衍生出丰富的 loader 和 plugin 可以满足各种使用场景

  • Rollup 更像一把手术刀,它更专注于 JavaScript 的打包。

    当然也支持其他类型的模块,但总体而言在通用性上还是不如 webpack。如果当前的项目需求仅仅是打包 JavaScript,比如一个 JavaScript 库,那么 Rollup 很多时候会是我们的第一选择。


rollup 的问题

对 CommonJS 的兼容问题

因为 rollup 原生只支持 ESM 模块的 bundle,因此如果实际业务中需要对 commonjs 进行 bundle,第一步就是需要将 CJS 转换成 ESM,不幸的是,Commonjs 和 ES Module 的 interop 问题是个非常棘手的问题(搜一搜 babel、rollup、typescript 等工具下关于 interop 的 issue:https://sokra.github.io/interop-test/

其两者语义上存在着天然的鸿沟,将 ESM 转换成 Commonjs 一般问题不太大(小心避开 default 导出问题),但是将 CJS 转换为 ESM 则存在着更多的问题。 实际上 rollup 也正在重写该核心模块:https://github.com/rollup/plugins/pull/658。 

一些典型的问题如下

  • 由于 commonjs 的导出模块并非是 live binding 的,所以导致一旦出现了 commonjs 的循环引用,则将其转换成 esm 就会出问题

  • 同步的动态 require 几乎无法转换为 esm,如果将其转换为 top-level 的 import,根据 import 的语义,bundler 需要将同步 require 的内容进行 hoist,但是这与同步 require 相违背,因此动态 require 也很难处理

cjs2esm 的复杂性,导致该转换算法十分复杂,导致一旦业务里包含了很多 cjs 的模块,rollup 其编译性能就会急剧下降,这在编译一些库的时候可能不是大问题,但是用于大型业务的开发,其编译速度难以接受。


parcel

Parcel 优点:

  • 极速打包:Parcel 使用 worker 进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。

  • 将你所有的资源打包:Parcel 具备开箱即用的对 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。

  • 自动转换:如若有需要,Babel, PostCSS, 和 PostHTML 甚至 node_modules 包会被用于自动转换代码.

  • 零配置代码分拆:使用动态 import()语法, Parcel 将你的输出文件束(bundles)分拆,因此你只需要在初次加载时加载你所需要的代码。

  • 热模块替换:Parcel 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。

  • 友好的错误日志:当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。

新建 index.html、index.js 和 index.css,然后 parcel index.html,就能拿到可运行的 html、js 和 css 组合。html 可以作为入口正是我期望的,这让前端开发回归到本来的状态,很舒服。

关于 0 配置。ParcelJS 本身是 0 配置的,但 HTML、JS 和 CSS 分别是通过 posthtml、babel 和 postcss 处理的,所以我们得分别配 .posthtmlrc、.babelrc 和 .postcssrc。

ParcelJS 是以 assets 方式组织的,assets 可以是任意文件,所以你可以构建任意文件。而在 webpack 中,只有 JS 是一等公民(webpack@4 会增加 CSS 为一等公民),所以必须是以 JS 为入口去组织其他文件,这很别扭。

parcel vs webpack

parcel 中的特性像是多进程、缓存等,其实都可以利用 Webpack 的一些相关模块搞定(Happypack、DllPlugin 等),但单从代码转译这一点上来说确实比 Webpack 要先进。

Webpack 慢的核心原因

Webpack 之所以有时感觉很慢,是因为代码转译全靠 loader 进行字符串处理。

比如一个 index.js 有可能要经历 loaderA -> loaderB -> loaderC,这些 loader 完全不知道彼此之间的存在,都是接过来一个字符串自己处理,然后再交给下一个。如果最后再 uglify 一下还要先 parse 为 AST(抽象语法树) 再压缩,这一步也是比较耗时的。用简单公式可以理解为(n 为需要 transform 的过程):

Webpack 打包时间 = parse string * n + transform * n + parse to AST + compress

parcel 比 webpack 快在哪里?

在 parcel 代码转译是先 parse 为 AST,然后再进行 transform。即便有多步转译流程,最后再加上 uglify,全部也只用 parse 一遍。用简单公式可以理解为(n 为需要 transform 的过程):

parcel 打包时间 = parse to AST + transform * n + compress

因此,parcel 至少为我们提供了一个很好的思路:多步转译 + 压缩时,每一步都可以利用到已经解析过后的 AST,只要完成各自的 transform 即可。

Parcel 最大的优势:因为 webpack 的每个 loader 都要生成一遍 AST,Parcel 则不用,只需生成一次 AST(相当于 Parcel 内置了 loader,才能做此优化)

esbuild

esbuild 是Evan Wallace( Figma 的 CTO)开发的。


Figma 的 CTO - Evan Wallace


其主要目的为提升建置速度,比起基于 Nodejs 的工具可达到 10 到 100 倍快。

为什么 esbuild 这么快 ?


为什么 esbuild 这么快 ?


它是用 Go 语言编写的,并可以编译为本地代码。

大多数前端打包工具都是基于 JavaScript 实现的,而 Esbuild 则选择使用 Go 语言编写,两种语言各自有其擅长的场景,但是在资源打包这种 CPU 密集场景下,Go 更具性能优。一般来说,JS 的操作是毫秒级,而 Go 则是纳秒级。

虽然现代 JS 引擎与 10 年前相比有巨大的提升,但 JavaScript 本质上依然是一门解释型语言,JavaScript 程序每次执行都需要先由解释器一边将源码翻译成机器语言,一边调度执行;而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要直接执行这些机器码即可。也就意味着,Go 语言编写的程序比 JavaScript 少了一个动态解释的过程。


JavaScript与Go对比


这种语言层面的差异在打包场景下特别突出,说的夸张一点,JavaScript 运行时还在解释代码的时候,Esbuild 已经在解析用户代码;JavaScript 运行时解释完代码刚准备启动的时候,Esbuild 可能已经打包完毕,退出进程了!

多线程优势

Go 天生具有多线程运行能力,而 JavaScript 本质上是一门单线程语言,直到引入 WebWorker 规范之后才有可能在浏览器、Node 中实现多线程操作。

 Rollup、Webpack 的代码,就我熟知的范围内两者均未使用 WebWorker 提供的多线程能力。反观 Esbuild,它最核心的卖点就是性能,它的实现算法经过非常精心的设计,尽可能饱和地使用各个 CPU 核,特别是打包过程的解析、代码生成阶段已经实现完全并行处理。

除了 CPU 指令运行层面的并行外,Go 语言多个线程之间还能共享相同的内存空间,而 JavaScript 的每个线程都有自己独有的内存堆。这意味着 Go 中多个处理单元,例如解析资源 A 的线程,可以直接读取资源 B 线程的运行结果,而在 JavaScript 中相同的操作需要调用通讯接口 woker.postMessage 在线程间复制数据。

  • Go 在线程之间共享内存,而 JavaScript 必须在线程之间序列化数据。

  • Go 和 JavaScript 都有并行的垃圾收集器,但是 Go 的堆在所有线程之间共享,而对于 JavaScript, 每个 JavaScript 线程中都有一个单独的堆。

根据测试,这似乎将 JavaScript worker 线程的并行能力减少了一半,大概是因为一半 CPU 核心正忙于为另一半收集垃圾。

esbuild 为什么不用 Rust,而使用了 Go?

bundler 这种事情,GC 未必是劣势。写一个打包工具,大部分的工作是字符串拼接和图遍历。对于图数据结构,GC 是一个很好的辅助工具。用 Rust/C++ 你得考虑非常多内存分配的细节

用 Rust/C++ 写过图的对此应该都有很深的体会。对于 Rust 这种尽量避免循环引用的语言,怎么表示图结构我猜现在还没有一个很好的方案吧。而一个成熟的 GC 帮你解决了这些问题。

Rust/C++ 这种无 GC 语言的在内存上优势则是在于分配和释放的稳定,但是性能(吞吐)上未必有优势。比如大量的内存分配的释放在 Rust/C++ 里面都是很慢的(当你 parsing 的时候)。因此你需要做很多优化,比如说内存池,而这些都是侵入式的,会让你的代码变得 ugly。

Go 还有一个优势是原生的轻量级线程的支持。这些 Rust/C++ 当然能实现,但是 Go 还实现了一个非常优秀的调度器,调度 IO 和计算。而给 Rust/C++ 的只有 native thread,如果你又想做一套调度,那又是何苦呢。

1、拿 rust 写代码确实心智负担很高,很多时候很难有内存去做高层的设计,此外 rust 的智能指针和 pattern match 的适配度很低,所以很多代码要缩进一层又一层

2、此外 rust 对复杂所有权的数据结构很不友好,而这对很多静态分析来说都是必要的。rustc 表示我选择躺平用 arena

3、esbuild 的代码为了效率,整个流程只过两遍 ast,代价就是代码写成一大坨,显然还是 babel/swc 这种传统编译器的分 pass 模式更方便扩展,他们提供的功能也更丰富

4、即便如此 esbuild 作为转译器的效率也没超过 swc,可以说是责任全在 go 的垃圾编译器/运行时上了

5、此外不支持 ADT 的语言(是的,包括 CPP)都不适合表达 AST,强行拿来写编译器属于是削足适履了,然而谁叫 ml 系没流行起来呢


大量使用了并行操作

esbuild 中的算法经过精心设计,可以充分利用 CPU 资源。

大致分为三个阶段:

  • 解析

  • 链接

  • 代码生成

解析和代码生成是大部分工作,并且可以完全并行化(链接在大多数情况下是固有的串行任务)。

由于所有线程共享内存,因此当捆绑导入同一 JavaScript 库的不同入口点时,可以轻松地共享工作。

大多数现代计算机具有多内核,因此并行性是一个巨大的胜利。


esbuild 的主要功能:

Esbuild 并不是另一个 Webpack,它仅仅提供了构建一个现代 Web 应用所需的最小功能集合,未来也不会大规模加入我们业已熟悉的各类构建特性。

  • Extreme speed without needing a cache

  • ES6 and CommonJS modules

  • Tree shaking of ES6 modules

  • An API for JavaScript and Go

  • TypeScript and JSX syntax

  • Source maps

  • Minification

  • Plugins

官网明确声明未来没有计划支持如下特性:

  • Elm, Svelte, Vue, Angular 等代码文件格式

  • Ts 类型检查

  • AST 相关操作 API

  • Hot Module Replace

  • Module Federation

Esbuild 所设计的插件系统也无意覆盖以上这些场景,这就意味着第三方开发者无法通过「插件」这种无侵入的方式实现上述功能

Esbuild 只解决一部分问题,所以它的架构复杂度相对较小,相对地编码复杂度也会小很多,相对于 Webpack、Rollup 等大一统的工具,也自然更容易把性能做到极致。节制的功能设计还能带来另外一个好处:完全为性能定制的各种附加工具。

Esbuild 的节制

回顾一下,在 Webpack、Rollup 这类工具中,我们不得不使用很多额外的第三方插件来解决各种工程需求,比如:

  • 使用 babel 实现 ES 版本转译

  • 使用 eslint 实现代码检查

  • 使用 TSC 实现 ts 代码转译与代码检查

  • 使用 less、stylus、sass 等 css 预处理工具

我们已经完全习惯了这种方式,甚至觉得事情就应该是这样的,大多数人可能根本没有意识到事情可以有另一种解决方案。Esbuild 起了个头,选择完全!Esbuild 完全重写整套编译流程所需要用到的所有工具!这意味着它需要重写 js、ts、jsx、json 等资源文件的加载、解析、链接、代码生成逻辑。

开发成本很高,而且可能被动陷入封闭的风险,但收益也是巨大的,它可以一路贯彻原则,以性能为最高优先级定制编译的各个阶段,比如说:

  • 重写 ts 转译工具,完全抛弃 ts 类型检查,只做代码转换

  • 大多数打包工具把词法分析、语法分析、符号声明等步骤拆解为多个高内聚低耦合的处理单元,各个模块职责分明,可读性、可维护性较高。而 Esbuild 则坚持性能第一原则,不惜采用反直觉的设计模式,将多个处理算法混合在一起降低编译过程数据流转所带来的性能损耗

  • 一致的数据结构,以及衍生出的高效缓存策略,下一节细讲

这种深度定制一方面降低了设计成本,能够保持编译链条的架构一致性;一方面能够贯彻性能第一的原则,确保每个环节以及环节之间交互性能的最优。虽然伴随着功能、可读性、可维护性层面的的牺牲,但在编译性能方面几乎做到了极致。

Esbuild 结构一致性

 Esbuild 选择重写包括 js、ts、jsx、css 等语言在内的转译工具,所以它更能保证源代码在编译步骤之间的结构一致性,比如在 Webpack 中使用 babel-loader 处理 JavaScript 代码时,可能需要经过多次数据转换:

  • Webpack 读入源码,此时为字符串形式

  • Babel 解析源码,转换为 AST 形式

  • Babel 将源码 AST 转换为低版本 AST

  • Babel 将低版本 AST generate 为低版本源码,字符串形式

  • Webpack 解析低版本源码

  • Webpack 将多个模块打包成最终产物

源码需要经历 string => AST => AST => string => AST => string ,在字符串与 AST 之间反复横跳。

而 Esbuild 重写大多数转译工具之后,能够在多个编译阶段共用相似的 AST 结构,尽可能减少字符串到 AST 的结构转换,提升内存使用效率。


Esbuild编译过程


Esbuild 适用情境

esbuild 颠覆了前端工具的世界。

  • 在大型项目中增加了几倍的编译速度是非常实用的。

  • 如果想要尽可能最小化编译档案的大小,使用 Rollup 和 terser,它们产出的档案稍微小一点。

Esbuild 当下与未来都不能替代 Webpack,它不适合直接用于生产环境,而更适合作为一种偏底层的模块打包工具,需要在它的基础上二次封装,扩展出一套既兼顾性能又有完备工程化能力的工具链,例如 Snowpack, Vite, SvelteKit, Remix Run 

其中最为著名的 Vite 和 snowpack 底层都是用了 esbuild。

Snowpack

Snowapck 是由SkypackPika(Pika 团队有一个宏伟的使命:让 Web 应用提速 90%)的作者开发的建置工具。核心功能是开发时期支援 Unbundled Development ,其概念是在开发时提供浏览器个别的档案。档案依旧可以使用 Babel,TypeScript,Sass 编译然后由浏览器个别载入,也就是当您变更档案时 Snowpack 只会重新编译该档,然后只重新载入该档。节录官方文件的说法:使用封装工具应该是您想要使用,而不是必须要使用。

首次提出利用浏览器原生 ESM 能力的工具并非是 Vite,而是一个叫做 Snowpack 的工具。前身是 @pika/web,从 1.x 版本开始更名为 Snowpack。

Snowpack 利用 JavaScript 的本机模块系统(称为 ESM)来避免不必要的工作并保持流畅的开发体验。

在 HTTP/2 和 5G 网络的加持下,我们可以预见到 HTTP 请求数量不再成为问题,而随着 Web 领域新标准的普及,浏览器也在逐步支持 ESM。

其中一个卖点就是加速开发。



Snowpack 不会将所有程式码封装打包成一个档案,浏览器载入个别档案。虽然 esbuild 确实是其中一个相依套件,但 Snowpack 的想法是使用原生 JavaScript 模组,直到你需要封装成一个档案的时候才使用 esbuild。

Snowpack 的理念是减少或避免整个 bundle 的打包,每次保存单个文件时,传统的 JavaScript 构建工具(例如 Webpack 和 Parcel)都需要重新构建和重新打包应用程序的整个 bundle。重新打包时增加了在保存更改和看到更改反映在浏览器之间的时间间隔。在开发过程中,Snowpack 为你的应用程序提供 unbundled server。每个文件只需要构建一次,就可以永久缓存。文件更改时,Snowpack 会重新构建该单个文件。在重新构建每次变更时没有任何的时间浪费,只需要在浏览器中进行 HMR 更新。

Snowpack 拥有美观的官方文件包含搭配其他框架的设定说明专案样版。一些教学还处于编写中,已完成的像React 教学 就非常清楚。另外 Snowpack 似乎以Svelete 为第一优先。事实上,我第一次听说 Snowpack 就是在 Svelte Submit 2020, Rich Harris 的 未来的网页开发。当时提到即将推出的SvelteKit应该会使用 Snowpack;后来选择了 Vite - SvelteKit is in public beta 说明

适用情境

想尝试 Unbundle Deployment 不想增加封装档的复杂度, Snowpack 是不错的选择。如果您正在开发的专案模组量不大,意味着不会产生大量个别编译的需求。一个不错的使用情境是;您正逐步采用 SSR 或静态应用程式。您可以只使用一点点 Node 生态圈的工具,但仍保留前端框架的好处。

第二,我认为 Snowpack 是一个不错的 esbuild 强化版。如果您想使用 esbuild 又想要好用的开发伺服器和专案样版,那么选 Snowpack 不会错。

Vite

Vite 是由 Vue 作者 Evan You 和 Hades speedruns 开发的。在 esbuild 专注在编译速度,Snowpack 专注开发伺服器。Vite 则提供两者;完整的开发伺服器和使用 Rollup 进行优化编译。

适用情境

如果您在寻找的是像 create-react-app 或 Vue CLI 的竞品,Vite 是最接近的一个,因为它内建包含这些功能。轻量快速的开发伺服器,零设定即支援正式版本优化。Vite 可以适用于小型的个人项目 Side-Project 或大型正式项目。

为什么不使用 Vite?

Vite 是一个坚持己见的工具,可能您不同意其中的一些观点。比如您不想使用 Rollup,或想使用上面提到非常快的 esbuild,或希望预设能提供完整 Babel ,eslint ,和 webpack loaders 生态圈的功能。还有如果您想使用无须额外设定的 Meta-frameworks,那么您最好继续使用基于 webpack 的框架,例如 Nuxt.js,Next.js 直到 Vite 的伺服器端渲染功能更完整。


webpack 和 esbuild 配合

有很多项目已经在他们当前的构建工具上投入了大量资金——主要是 webpack。迁移到新的构建工具并非易事。新项目可能会从 Vite 开始,但现有项目不太可能被移植。

esbuild-loader hiroki osame开发,是一个建立在 esbuild 之上的 webpack 加载器。它允许用户通过它交换 ts-loader 或 babel-loader,这大大提高了构建速度。

具体查看《webpack 或 esbuild:为什么不是两者兼而有之? https://xie.infoq.cn/article/d9c4ca69e0de8fecf176dfd20


转载本站文章《新一代构建工具(1):对比rollup/parcel/esbuild—esbuild脱颖而出》,请注明出处:https://www.zhoulujun.cn/html/tools/Bundler/vite/8770.html

发布于: 刚刚阅读数: 3
用户头像

zhoulujun

关注

还未添加个人签名 2021.06.25 加入

还未添加个人简介

评论

发布
暂无评论
新一代构建工具(1):对比rollup/parcel/esbuild—esbuild脱颖而出_Rollup_zhoulujun_InfoQ写作社区