当前位置: 首页 > 科技观察

新一代前端搭建,Esbuild还能这么玩!

时间:2023-03-18 14:37:48 科技观察

大家好,我是三元同学。之前停了一段时间,因为得了流感,一直在家养病,一直没来得及更新文章。想对各位读者说一声对不起~今天给大家带来一篇我最近写的原创文章,因为最近在研究前端搭建相关的领域,比如Esbuild和Vite,接触的比较多,而这些工具现在在前端圈子里很流行,也引起了业界的关注,所以我觉得有必要把自己研究的一些东西分享给大家,希望对你有帮助。什么是Esbuild?Esbuild是Figma的CTO“EvanWallace”基于Golang开发的打包工具。与传统打包工具相比,Esbuild具有主要性能优势,构建速度可提高10~100倍。架构优势1.Golang是使用Go语言开发的。与单线程+JIT解释型语言相比,使用Go的优势在于:一方面可以充分利用多线程封装,线程间共享内容,而JS使用多线程也需要线程的开销通信(postMessage);另一方面是直接编译成机器码,而不是像Node一样先把JS代码解析成字节码再转成机器码,大大节省了程序运行时间。2、多核并行内装算法,充分发挥多核CPU的优势。Esbuild内部算法设计经过精心设计,尽可能充分利用所有CPU内核。所有步骤尽可能并行化,这也得益于Go中多线程共享内存的优势,而JS中所有步骤只能串行化。3.从头造轮子从头造轮子从头造轮子,没有任何第三方库的黑盒逻辑,保证极致的代码性能。4.内存的高效利用一般来说,传统的JS开发的打包工具中,AST数据的解析和传递比较频繁,比如string->TS->JS->string,这涉及到复杂的编译工具链,比如webpack->babel->terser,每次接触新的工具链,都要重新解析AST,导致大量内存占用。在Esbuild中,一个AST节点数据从头到尾都尽可能的复用,大大提高了内存利用效率,提高了编译性能。与SWC对比速度,我们使用纯Esbuild和SWC来编译代码,将其作为Transformer转换800+tsx文件,无需编写任何JS胶水代码(如esbuild-register、esbuild-loader、swc-loader本身为了适配相应的宿主工具,会写一堆JS胶水代码,影响判断)。从这个例子可以看出,Esbuild和SWC在性能上是同一个数量级的。这里通过仓库示例,Esbuild略快,但不排除SWC在其他示例中略快于Esbuild的场景。兼容性Esbuild本身的局限性包括:没有TS类型检查不能操作AST不支持装饰器语法producttarget不能降级到ES5及以下版本,这意味着需要ES5产品的场景只能由Esbuild使用。相比之下,SWC具有更好的兼容性:产品支持ES5格式,支持装饰器语法,可以通过编写JS插件进行操作。AST应用场景对于Esbuild和SWC,很多时候我们比较两者的性能而忽略了应用场景。前端构建工具,主要有几个垂直功能:BundlerTransformerMinimizer从上面的速度和兼容性对比可以看出,Esbuild和SWC在transformer方面的性能差不多,但是Esbuild的兼容性远不如SWC。因此,SWC更适合做Transformer。但是作为一个Bundler和Minimizer,SWC是捉襟见肘的。首先官方swcpack目前基本无法使用,Minimizer也很不成熟,很容易遇到兼容性问题。Esbuild作为Bundler,在开发阶段已经被Vite用作依赖预打包工具,也被广泛用作在线esmCDN服务,如esm.sh等;作为Minimizer,Esbuild已经足够成熟,已经被Vite作为生产环境使用的JS和CSS代码压缩工具。整体来看,SWC和Esbuild的关系类似于现在的Babel和Webpack。前者更适合兼容性和定制化要求高的Transformer(比如移动业务场景),后者适合Bundler和Minimizer,以及兼容性和定制化要求不高的Transformer。插件机制esbuild插件是一个对象,有name和setup两个属性。name是插件的名字,setup是一个函数。输入参数是一个构建对象。这个对象上挂载了一些钩子供我们自定义。构建逻辑。下面是一个简单的esbuild插件示例:letenvPlugin={name:'env',setup(build){//解析文件时触发//限制插件的作用域为env文件,并为其标识命名空间”env-ns"build.onResolve({filter:/^env$/},args=>({path:args.path,namespace:'env-ns',}))//文件加载时触发//只有namespace是"env-ns"的文件才会被处理//将process.env对象反序列化成字符串交给json-loader处理build.onLoad({filter:/.*/,namespace:'env-ns'},()=>({contents:JSON.stringify(process.env),loader:'json',}))},}require('esbuild').build({entryPoints:['app.js'],bundle:true,outfile:'out.js',//应用插件plugins:[envPlugin],}).catch(()=>process.exit(1))使用如下:*//应用env插件后,在构建时会被process.env对象替换*import{PATH}from'env'console.log(`PATHis${PATH}`)不过写的时候有几点需要注意插件:Esbuild插件机制m只能作用于buildAPI,不能作用于transformAPI,也就是说webpack中的esbuild-loader,只使用了Esbuildtransform功能,无法利用Esbuild的插件机制。插件中的filterregex是使用go原生的regex实现的,用于过滤文件。为了不过度降低性能,规则应该尽可能严格。同时,它也不同于JS正则化,例如不支持向前看(?<=)、向后看(?=)和向后引用(\1)。实际的插件应该考虑自定义缓存(以减少负载重复开销)、sourcemap合并(源代码正确映射)和错误处理。可以参考Svelte插件。虚拟模块支持相对于Rollup,作为一个打包器,一般需要两种模块,一种存在于真实的磁盘文件系统中,另一种不存在于磁盘中,而是存在于内存中,即虚拟模块。Rollup本身天然支持虚拟模块。基于其插件机制,Vite也大量使用了虚拟模块的功能。以处理wasm文件为例:constwasmHelperId='/__vite-wasm-helper'//helper函数实现constwasmHelper=async(opts={},url:string)=>{//省略具体实现}exportconstwasmPlugin=(配置:ResolvedConfig):Plugin=>{return{name:'vite:wasm',resolveId(id){if(id===wasmHelperId){returnid}},asyncload(id){if(id===wasmHelperId){return`exportdefault${wasmHelperCode}`}if(!id.endsWith('.wasm')){return}consturl=awaitfileToUrl(id,config,this)//虚拟模块return`importinitWasmfrom"${wasmHelperId}"exportdefaultopts=>initWasm(opts,${JSON.stringify(url)})`}}}但是Rollup的虚拟模块也有一些限制,为了区别于真实模块,默认约定是放一个'\路径前面的0'。这样会对路径有一定的侵入性,直接导入浏览器会出问题(Vite也将这种形式的\0替换为__xx,以免直接将带\0的路径导入浏览器):Esbuild中的virtualmodules比较友好,通过命名空间直接区分真实模块和虚拟模块,所以不会出现\0等hack操作。编译能力使用Esbuild的虚拟模块,可以完成很多功能。上述插件示例除了计算内存中env的值作为模块内容外,还可以将模块名编译为函数,甚至在编译阶段实现函数。递归过程。例如,这个Esbuild插件:{name:'fibo',setup(build){build.onResolve({filter:/^fib\(\d+\)/},args=>{return{path:args.path,命名空间:'fib'}})build.onLoad({filter:/^fib\(\d+\)/,namespace:'fib'},args=>{constmatch=/^fib\((\d+)\)/.exec(args.path);n=Number(match[1]);console.log(n);letcontents=n<2?`exportdefault${n+1}`:`importn1from'fib(${n-1})'importn2from'fib(${n-2})'exportdefaultn1+n2`return{contents}})}}引入这个插件,可以解析如下导入语句:importfib5from'fib(5)'console.log(fib5)//13所有模块都是虚拟模块,不存在于真实的文件系统中。另外,虚拟模块也可以用于URLImport,支持如下导入代码:importReactfrom'https://esm.sh/react@17'这个也可以在插件中实现,请参考示例.场景一、代码压缩工具Esbuild的代码压缩功能非常好,可以摆脱传统压缩工具一个数量级以上的性能差距。Vite也在2.6版本正式宣布可以在生产环境直接使用Esbuild压缩JS和CSS代码。2.代替ts-node社区,esno已经有相应的解决方案:https://github.com/antfu/esnots-nodeindex.ts//替换为esnohello.ts3。而不是ts-jest,使用esbuild-jest而不是ts-jest,我尝试在一些大型包中使用esbuild-jest作为转换器。与ts-jest相比,整体测试效率提高了3倍左右。Github地址:https://github.com/aelbore/esbuild-jest4。第三方库BundlerVite在开发阶段使用Esbuild对依赖进行预打包,将所有使用到的第三方依赖转化为ESM格式的Bundle产品,并计划在未来生产环境中使用。同时,业界也有一些平台基于纯Esbuild提供在线cjs->esmCDN服务,例如esm.sh和skypack:5.打包Node库为什么要打包Node库:减少node_modules代码并且避免业务安装大量的node_modules代码,减少安装体积,提高启动速度。所有代码都保存在一个文件中,减少了大量的文件io操作,更安全。所有代码打包也是一种锁定依赖版本的方式,可以避免之前出现的coa包导致大规模CI挂起的问题。可以参考云倩的这篇文章。在这方面Esbuild的作用类似于vercel团队出品的ncc,但是对代码的写法有一些限制,无法分析动态require或import语句包含变量的情况:6.小程序编译对于小程序场景,也可以使用Esbuild代替Webpack,大大提高编译速度。对于AST转换,在SWC中嵌入Esbuild插件,实现快速编译。详见esbuild上的132分享制作。7、Web搭建Web场景比较复杂,对兼容性和周边工具生态要求比较高,比如低浏览器语法退化、CSS预编译器、HMR等,如果想用纯Esbuild来做,你还需要加很多能力。之前三元实现了一套基于Esbuild的web开发脚手架ewas,已经开源在Github上,在我之前的小册子项目中已经成功实现。与create-react-app相比,启动速度提升了100多倍(30s->0.3s)。仓库地址:https://github.com/sanyuan0704/ewas。现在Remix1.0正式发布,底层使用Esbuild构建,带来极致的性能体验,成为Next.js的有力竞争者。但总的来说,目前Esbuild还有很多能力不支持真正的web场景,还存在一些缺陷,包括不支持降级到ES5的语法、解包不灵活、不支持HMR等。对于真正可以作为Webpack使用的构建,在工具方面还有很长的路要走。