新一代构建工具(1):对比rollup/parcel/esbuild—esbuild脱颖而出
Author:zhoulujun Date:
文章内容来源:
字节前端是如何基于 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过去一年情形
Stars | Issues | 版本 | Updated | Created | |
---|---|---|---|---|---|
esbuild | 30,793 | 240 | 0.14.25 | 3天前 | 4年前 |
parcel | 40,314 | 758 | 2.3.2 | 17天前 | 9年前 |
rollup | 21,301 | 382 | 2.70.0 | 8小时前 | 7年前 |
snowpack | 19,104 | 270 | 3.8.8 | 6月前 | 2年前 |
webpack | 60,579 | 287 | 5.70.0 | 4天前 | 10年前 |
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)开发的。
其主要目的为提升建置速度,比起基于Nodejs 的工具可达到10 到100 倍快。
为什么 esbuild 这么快 ?
它是用 Go 语言编写的,并可以编译为本地代码。
大多数前端打包工具都是基于 JavaScript 实现的,而 Esbuild 则选择使用 Go 语言编写,两种语言各自有其擅长的场景,但是在资源打包这种 CPU 密集场景下,Go 更具性能优。一般来说,JS 的操作是毫秒级,而 Go 则是纳秒级。
虽然现代 JS 引擎与10年前相比有巨大的提升,但 JavaScript 本质上依然是一门解释型语言,JavaScript 程序每次执行都需要先由解释器一边将源码翻译成机器语言,一边调度执行;而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要直接执行这些机器码即可。也就意味着,Go 语言编写的程序比 JavaScript 少了一个动态解释的过程。
这种语言层面的差异在打包场景下特别突出,说的夸张一点,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 颠覆了前端工具的世界。
在大型项目中增加了几倍的编译速度是非常实用的。
如果想要尽可能最小化编译档案的大小,使用Rollup 和terser,它们产出的档案稍微小一点。
Esbuild 当下与未来都不能替代 Webpack,它不适合直接用于生产环境,而更适合作为一种偏底层的模块打包工具,需要在它的基础上二次封装,扩展出一套既兼顾性能又有完备工程化能力的工具链,例如 Snowpack, Vite, SvelteKit, Remix Run
其中最为著名的Vite和snowpack底层都是用了esbuild。
Snowpack
Snowapck 是由Skypack和Pika(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