什么是Esbuild?Esbuild是一个非常新的模块打包工具。它提供了与Webpack、Rollup、Parcel等工具“相似”的资源打包能力,却有着一个离谱的高性能优势:从上到下,耗时逐渐增加,相差数百倍。这种巨大的性能优势使得Esbuild迅速在众多基于Node的构建工具中流行起来。尤其是在Vite2.0宣布使用Esbuild预构建依赖后,前端社区议论纷纷。讨论迅速增加。那么问题来了,这是怎么做到的?看了很多资料,总结了一些关键因素:下面会详细解释。为什么是快语言的优势大多数前端打包工具都是基于JavaScript实现的,而Esbuild则选择使用Go语言来编写。两种语言各有各的好场景,但在资源打包这种CPU密集型场景,Go更有性能优势,差距有多大呢?比如计算斐波那契数列50次,JS版:functionfibonacci(num){if(num<2){return1}returnfibonacci(num-1)+fibonacci(num-2)}(()=>{letcursor=0;while(cursor<50){fibonacci(cursor++)}})()Go版本:packagemainfuncfibonacci(numint)int{ifnum<2{return1}returnfibonacci(num-1)+fibonacci(num-2)}funcmain(){fori:=0;i<50;i++{fibonacci(i)}}JavaScript版本的执行时间约为“332.58s”,Go版本的执行时间约为“147.08s”。差异大约是“1.25”倍。这个简单的实验并不能准确量化两种语言的性能差异,但是从感官上还是可以明显看出Go语言在CPU密集型场景下会有更好的表现。归根结底,虽然现代JS引擎与10年前相比有了很大的进步,但JavaScript本质上仍然是一种解释型语言。每次执行JavaScript程序时,都需要解释器将源代码翻译成机器语言,同时调度执行;而Go是一种编译型语言,源代码在编译阶段已经被翻译成机器码,这些机器码只需要在启动时直接执行即可。这种语言层面的差异在打包场景中尤为突出。说得夸张一点,当JavaScriptruntime还在解释代码的时候,Esbuild已经在解析用户代码了;打包完成,进程退出!所以在编译运行层面,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最新版本的主要特点是:支持js、ts、jsx、css、json、text、pictures等资源增量更新Sourcemap开发服务器支持代码压缩CodesplitTreeshaking插件支持可以看到,这个列表中支持的资源类型和工程特性非常少,甚至不足以支撑一个大型项目的开发需求。另外,官网明确表示未来没有计划支持以下特性:Elm、Svelte、Vue、Angular等代码文件格式Ts类型检查AST相关操作APIHotModuleReplaceModuleFederation而且,插件Esbuild设计的-in系统并不打算涵盖以上这些场景,这意味着第三方开发者无法以“插件”的非侵入方式实现上述功能。emmm,可以预见,以后可能会有很多魔改版本。Esbuild只解决了一部分问题,所以它的架构复杂度比较小,编码复杂度也比较小。相比Webpack、Rollup等统一工具,自然更容易做到极致的性能。内敛的功能设计还带来了另一个好处:各种完全为性能定制的附加工具。定制化回顾一下,在Webpack、Rollup等工具中,我们不得不使用很多额外的第三方插件来解决各种工程需求,例如:使用babel实现ES版本转换使用eslint实现代码检查使用TSC实现tscodetranslationandCodeinspectionusingless,stylus,sass等css预处理工具我们已经完全习惯了这种方式,甚至觉得事情应该是这样的,大多数人甚至可能没有意识到事情可以有另一种解决方案。Esbuild启动,选择Complete!完全重写整个编译过程所需的所有工具!这意味着它需要重写js、ts、jsx、json等资源文件的加载、解析、链接、代码生成逻辑。开发成本高,可能会被动陷入被关闭的风险,但收益也是巨大的。可以一路贯彻原理,自定义以性能为最高优先级的编译各个阶段。例如:重写ts翻译工具,完全摒弃ts类型Inspection,只进行代码转换大多数打包工具将词法分析、语法分析、符号声明等步骤拆解成多个处理单元,高内聚低耦合。各模块职责明确,具有较高的可读性和可维护性。而Esbuild则秉承性能至上的原则,不惜采用反直觉的设计模式,将多种处理算法混合在一起,以减少编译过程中数据流带来的性能损失。一致的数据结构,衍生出高效的缓存策略,下面这种深度定制降低了设计成本,同时保持了编译链的架构一致性;另一方面,可以贯彻性能优先的原则,保证各环节的性能最优和环节之间的交互。虽然伴随着功能性、可读性、可维护性等方面的牺牲,但在编译性能上几乎做到了极致。结构一致性上一节我们提到Esbuild选择重写翻译工具包括js、ts、jsx、css等语言,这样可以更好的保证编译步骤之间源码的结构一致性,比如在Webpack中使用babel时-loader处理JavaScript代码,可能需要多次数据转换:Webpack读入源码,此时Babel解析字符串形式的源码,转换为AST形式Babel将源码AST转换为低版本ASTBabel将低版本AST转换成低版本源码,Webpack以字符串形式解析低版本源码。webpack将多个模块打包成最终产品源码需要经过string=>AST=>AST=>string=>AST=>string,string和AST之间反复跳转。Esbuild重写了大部分翻译工具后,可以在多个编译阶段共享相似的AST结构,尽可能减少字符串到AST结构的转换,提高内存使用效率。总结单从编译性能来看,Esbuild确实优于世界上所有的打包框架,差距甚至可以大上百倍:但这是有代价的。除了语言层面的天然优势外,直接放弃了功能层面。Less,对stylus、sass、vue、angular等资源的支持,放弃MF、HMR、TS类型检查等功能,正如作者所言:?这将涉及对添加major的请求说“不”esbuild本身的功能。我不认为esbuild应该成为满足所有前端需求的一体化解决方案!?在我看来,Esbuild目前和未来都无法取代Webpack。不适合在生产环境直接使用,更适合作为一个底层模块封装工具,需要在它的基础上重新封装工具,扩展出一套兼顾性能和完整工程能力的工具链,如Snowpack、Vite、SvelteKit、RemixRun等。总的来说,Esbuild提供了一种全新的设计思路,值得学习和理解,但在大部分业务场景下并不适合直接生产使用。
