全文 2500 字,阅读耗时约 20 分钟,欢迎点赞关注转发。
Esbuild 是一个很是新的模块打包工具,它提供了与 Webpack、Rollup、Parcel 等工具类似的资源打包能力,却有着高的离谱的性能优点:javascript
从上到下,耗时逐步上升达到数百倍的差别,这个巨大的性能优点使得 Esbuild 在一众基于 Node 的构建工具中迅速蹿红,特别是 Vite 2.0 宣布使用 Esbuild 预构建依赖后,前端社区关于它的讨论热度逐渐上升。css
那么问题来了,这是怎么作到的?我翻阅了不少资料后,总结了一些关键因素:前端
下面展开细讲。vue
大多数前端打包工具都是基于 JavaScript 实现的,而 Esbuild 则选择使用 Go 语言编写,两种语言各自有其擅长的场景,可是在资源打包这种 CPU 密集场景下,Go 更具性能优点,差距有多大呢?好比计算 50 次斐波那契数列,JS 版本:java
function fibonacci(num) { if (num < 2) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2) } (() => { let cursor = 0; while (cursor < 50) { fibonacci(cursor++) } })()
Go 版本:webpack
package main func fibonacci(num int) int{ if num<2{ return 1 } return fibonacci(num-1) + fibonacci(num-2) } func main(){ for i := 0; i<50; i++{ fibonacci(i) } }
JavaScript 版本执行耗时大约为 332.58s,Go 版本执行耗时大约为 147.08s,二者相差约 1.25 倍,这个简单实验并不能精肯定量两种语言的性能差异,但感官上仍是能明显感知 Go 语言在 CPU 密集场景下会有更好的性能表现。git
归根到底,虽然现代 JS 引擎与10年前相比有巨大的提高,但 JavaScript 本质上依然是一门解释型语言,JavaScript 程序每次执行都须要先由解释器一边将源码翻译成机器语言,一边调度执行;而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只须要直接执行这些机器码便可。也就意味着,Go 语言编写的程序比 JavaScript 少了一个动态解释的过程。github
这种语言层面的差别在打包场景下特别突出,说的夸张一点,JavaScript 运行时还在解释代码的时候,Esbuild 已经在解析用户代码;JavaScript 运行时解释完代码刚准备启动的时候,Esbuild 可能已经打包完毕,退出进程了!web
因此在编译运行层面,Go 前置了源码编译过程,相对 JavaScript 边解释边运行的方式有更高的执行性能。算法
Go 天生具备多线程运行能力,而 JavaScript 本质上是一门单线程语言,直到引入 WebWorker 规范以后才有可能在浏览器、Node 中实现多线程操做。
我曾经研读过 Rollup、Webpack 的代码,就我熟知的范围内二者均未使用 WebWorker 提供的多线程能力。反观 Esbuild,它最核心的卖点就是性能,它的实现算法通过很是精心的设计,尽量饱和地使用各个 CPU 核,特别是打包过程的解析、代码生成阶段已经实现彻底并行处理。
除了 CPU 指令运行层面的并行外,Go 语言多个线程之间还能共享相同的内存空间,而 JavaScript 的每一个线程都有本身独有的内存堆。这意味着 Go 中多个处理单元,例如解析资源 A 的线程,能够直接读取资源 B 线程的运行结果,而在 JavaScript 中相同的操做须要调用通信接口 woker.postMessage
在线程间复制数据。
因此在运行时层面,Go 拥有自然的多线程能力,更高效的内存使用率,也就意味着更高的运行性能。
对,没错,节制!
Esbuild 并非另外一个 Webpack,它仅仅提供了构建一个现代 Web 应用所需的最小功能集合,将来也不会大规模加入咱们业已熟悉的各种构建特性。最新版本 Esbuild 的主要功能特性有:
能够看到,这份列表中支持的资源类型、工程化特性很是少,甚至并不足以支撑一个大型项目的开发需求。在这以外,官网明确声明将来没有计划支持以下特性:
并且,Esbuild 所设计的插件系统也无心覆盖以上这些场景,这就意味着第三方开发者没法经过插件这种无侵入的方式实现上述功能,emmm,能够预见将来可能会出现不少魔改版本。
Esbuild 只解决一部分问题,因此它的架构复杂度相对较小,相对地编码复杂度也会小不少,相对于 Webpack、Rollup 等大一统的工具,也天然更容易把性能作到极致。节制的功能设计还能带来另一个好处:彻底为性能定制的各类附加工具。
回顾一下,在 Webpack、Rollup 这类工具中,咱们不得不使用不少额外的第三方插件来解决各类工程需求,好比:
咱们已经彻底习惯了这种方式,甚至以为事情就应该是这样的,大多数人可能根本没有意识到事情能够有另外一种解决方案。Esbuild 起了个头,选择彻底!彻底重写整套编译流程所须要用到的全部工具!这意味着它须要重写 js、ts、jsx、json 等资源文件的加载、解析、连接、代码生成逻辑。
开发成本很高,并且可能被动陷入封闭的风险,但收益也是巨大的,它能够一路贯彻原则,以性能为最高优先级定制编译的各个阶段,好比说:
这种深度定制一方面下降了设计成本,可以保持编译链条的架构一致性;一方面可以贯彻性能第一的原则,确保每一个环节以及环节之间交互性能的最优。虽然伴随着功能、可读性、可维护性层面的的牺牲,但在编译性能方面几乎作到了极致。
上一节咱们讲到 Esbuild 选择重写包括 js、ts、jsx、css 等语言在内的转译工具,因此它更能保证源代码在编译步骤之间的结构一致性,好比在 Webpack 中使用 babel-loader 处理 JavaScript 代码时,可能须要通过屡次数据转换:
源码须要经历 string => AST => AST => string => AST => string
,在字符串与 AST 之间反复横跳。
而 Esbuild 重写大多数转译工具以后,可以在多个编译阶段共用类似的 AST 结构,尽量减小字符串到 AST 的结构转换,提高内存使用效率。
单纯从编译性能的维度看,Esbuild 确实完胜世面上全部打包框架,差距甚至能在百倍之大:
耗时 | 性能差别 | 速度 | 产物大小 | |
---|---|---|---|---|
esbuild | 0.11s | 1x | 1198.5 kloc/s | 0.97mb |
esbuild (1 thread) | 0.40s | 4x | 329.6 kloc/s | 0.97mb |
webpack 4 | 19.14s | 174x | 6.9 kloc/s | 1.26mb |
parcel 1 | 22.41s | 204x | 5.9 kloc/s | 1.56mb |
webpack 5 | 25.61s | 233x | 5.1 kloc/s | 1.26mb |
parcel 2 | 31.39s | 285x | 4.2 kloc/s | 0.97mb |
但这是有代价的,刨除语言层面的自然优点外,在功能层面它直接放弃对 less、stylus、sass、vue、angular 等资源的支持,放弃 MF、HMR、TS 类型检查等功能,正如做者所说:
This will involve saying "no" to requests for adding major features to esbuild itself. I don't think esbuild should become an all-in-one solution for all frontend needs\!
在我看来,Esbuild 当下与将来都不能替代 Webpack,它不适合直接用于生产环境,而更适合做为一种偏底层的模块打包工具,须要在它的基础上二次封装,扩展出一套既兼顾性能又有完备工程化能力的工具链,例如 Snowpack, Vite, SvelteKit, Remix Run 等。
总的来讲,Esbuild 提供了一种新的设计思路,值得学习了解,但对大多数业务场景还不适合直接投入生产使用。