为什么Esbuild快速?

2021-02-17 18:25:39

这是有关esbuild的常见问题的集合。您还可以在GitHub问题跟踪器上提问。

其他大多数捆绑程序都是用JavaScript编写的,但是对于JIT编译的语言,命令行应用程序是最坏的性能情况。每次运行捆绑程序时,JavaScript VM都会首次看到捆绑程序的代码,而没有任何优化提示。 esbuild忙于解析JavaScript时,node忙于解析捆绑程序的JavaScript。到节点完成对捆绑程序代码的解析时,esbuild可能已经退出,并且捆绑程序甚至还没有开始捆绑。

另外,Go是为并行性而设计的,而JavaScript不是。 Go在线程之间共享内存,而JavaScript必须在线程之间序列化数据。 Go和JavaScript都有并行的垃圾收集器,但是Go的堆在所有线程之间共享,而JavaScript每个JavaScript线程都有一个单独的堆。根据我的测试,这似乎将JavaScript工作者线程的并行能力减少了一半,这大概是因为您的CPU核心中有一半正忙于为另一半收集垃圾。

esbuild中的算法经过精心设计,以尽可能使所有可用的CPU内核完全饱和。大致分为三个阶段:解析,链接和代码生成。解析和代码生成是大部分工作,并且可以完全并行化(链接在大多数情况下是固有的串行任务)。由于所有线程共享内存,因此当捆绑导入相同JavaScript库的不同入口点时,可以轻松地共享工作。大多数现代计算机具有许多内核,因此并行性是一个巨大的胜利。

自行编写所有内容而不是使用3rd-party库,可以带来很多性能优势。您可以从一开始就牢记性能,可以确保一切都使用一致的数据结构来避免昂贵的转换,并且可以在必要时进行广泛的体系结构更改。缺点当然是很多工作。

例如,许多捆绑程序使用官方的TypeScript编译器作为解析器。但是,它是为实现TypeScript编译器团队的目标而构建的,它们没有将性能放在首位。他们的代码大量使用了巨形对象形状和不必要的动态属性访问(众所周知的JavaScript减速)。而且即使禁用类型检查,TypeScript解析器也似乎仍在运行类型检查器。这些都不是esbuild的自定义TypeScript解析器的问题。

理想情况下,编译器在输入长度上的复杂度通常为O(n)。因此,如果您要处理大量数据,则内存访问速度可能会严重影响性能。您必须对数据进行的遍历次数越少(将数据转换成数据所需的不同表示形式也就越少),编译器就会越快。

当AST数据在CPU缓存中仍然很热时,这可以最大限度地重用AST数据。其他捆绑程序在单独的遍历中执行这些步骤,而不是对它们进行交织。它们还可以在数据表示之间进行转换,以将多个库粘合在一起(例如,字符串→TS→JS→字符串,然后字符串→JS→旧JS→字符串,然后字符串→JS→最小化JS→字符串),这会占用更多的内存并减慢速度。

Go的另一个好处是,它可以将内容紧凑地存储在内存中,这使其可以使用更少的内存并在CPU缓存中容纳更多内存。所有对象字段的类型和字段都紧密地包装在一起,例如几个布尔标志每个仅占用一个字节。 Go也具有值语义,可以将一个对象直接嵌入另一个对象,因此它是免费的。没有其他分配。 JavaScript不具有这些功能,还具有其他缺点,例如JIT开销(例如,隐藏的类槽)和低效的表示形式(例如,非整数与指针一起分配在堆中)。

这些因素中的每一个都只是某种程度的显着提速,但是它们一起可以使捆扎机比当今常用的其他捆扎机快多个数量级。

该基准测试通过将three.js库复制10次并从头开始构建单个捆绑包而没有任何缓存,从而近似了一个大型JavaScript代码库。可以在esbuild存储库中使用make Bench-3运行基准测试。

每次报告是三个运行中最好的一次。我正在使用--bundle --minify --sourcemap运行esbuild(单线程版本使用GOMAXPROCS = 1)。我使用了rollup-插件插件,因为Rollup本身不支持缩小。 Webpack使用--mode =生产--devtool =源映射。包裹使用默认选项。绝对速度基于总行数(包括注释和空白行),当前为547,441。测试是在配备16GB RAM的6核2019 MacBook Pro上完成的。

该基准使用Rome构建工具来近似大型TypeScript代码库。必须将所有代码与源映射合并到一个缩小的捆绑包中,并且生成的捆绑包必须能够正常工作。可以在esbuild存储库中使用make bench-rome运行基准测试。

每次报告是三个运行中最好的一次。我正在使用--bundle --minify --sourcemap --platform =节点运行esbuild(单线程版本使用GOMAXPROCS = 1)。 Webpack将ts-loader与transpileOnly一起使用:true和--mode = production --devtool = sourcemap。宗地1使用--target node --bundle- node-模块。宗地2使用" engines&#34 ;:" node"在package.json中,并且需要@ parcel / Translator- typescript- tsc转换器才能处理基准测试中使用的TypeScript代码。绝对速度基于总行数(包括注释和空白行),当前为131,836。测试是在配备16GB RAM的6核2019 MacBook Pro上完成的。

结果不包含汇总,因为我无法使其正常工作。我尝试了rollup-插件打字稿,@ rollup /插件打字稿和@ rollup /插件sucrase,但由于与TypeScript编译相关的不同原因,它们都没有起作用。

这些是未来的潜在功能,但可能不会发生或可能会在更有限的程度上发生:

在那之后,我将认为esbuild比较完整。我正在计划esbuild达到大致稳定的状态,然后停止累积更多功能。这将涉及说" no"要求向esbuild本身添加主要功能。我不认为esbuild应该成为满足所有前端需求的一体化解决方案。特别是,我想避免&webpack配置"带来的麻烦和问题。底层工具过于灵活且易用性受损的模型。

我希望我添加到esbuild(插件和API)中的可扩展性点将使esbuild有用,以作为更多自定义构建工作流的一部分包括在内,但是我不打算或期望这些可扩展性点能够覆盖所有用例。如果您有非常自定义的要求,则应使用其他工具。我也希望esbuild能够启发其他构建工具,通过彻底检查其实现来极大地提高性能,从而使每个人都能受益,而不仅仅是使用esbuild的那些。

我计划即使在esbuild达到稳定后,也将继续维护esbuild现有范围内的所有内容。例如,这意味着实现对新发布的JavaScript和TypeScript语法功能的支持。

该项目尚未达到1.0.0版本,仍在积极开发中。也就是说,它远远超出了alpha阶段,并且非常稳定。我认为这是后期测试版。对于一些早期采用者而言,这意味着它足以用于真实事物。其他人则认为这意味着esbuild尚未准备就绪。本节不会尝试用任何一种方法说服您。它只是试图为您提供足够的信息,以便您可以自己决定是否要将esbuild用作捆绑程序。

由其他项目使用该API已在某些其他开发人员工具中用作库。例如,Vite和Snowpack正在使用esbuild的转换API将TypeScript转换为JavaScript。 Hugo正在使用esbuild的捆绑程序在构建过程中打包JavaScript代码。我还听说过其他人成功在生产中使用它的报道,尽管我不知道细节。一旦具有足够的功能,我打算在生产环境中自己使用esbuild,但尚未完成。

API的稳定性即使esbuild的版本不是1.0.0,仍会努力保持API的稳定性。修补程序版本用于向后兼容的更改,次要版本用于向后不兼容的更改(由npm建议)。如果您打算将esbuild用于真实的东西,则应该固定确切的版本(最大安全性)或固定主要版本和次要版本(仅接受向后兼容的升级)。

只有一个主要开发人员该工具主要由我构建。对于某些人来说,这很好,但是对于其他人,这意味着esbuild对于他们的组织而言不是合适的工具。我可以。我之所以建立esbuild是因为我发现它的构建很有趣,并且因为它是我想使用的工具。我之所以与全世界分享它,是因为还有其他人也想使用它,因为反馈使工具本身变得更好,并且因为我认为它将激发生态系统来制造更好的工具。

我并不总是打算扩大范围,我不打算包括我不希望构建和/或维护的主要功能。从架构的角度,测试和正确性的角度以及从可用性的角度来看,我还想限制项目的范围,以免变得过于复杂和笨拙。将esbuild视为一个&linker"用于网络。它知道如何转换和捆绑JavaScript和CSS。但是,您的源代码如何以纯JavaScript或CSS结尾的详细信息可能需要是第三方代码。

我希望插件将使社区能够添加主要功能(例如WebAssembly导入),而无需为esbuild本身做出贡献。但是,并非插件API公开了所有内容,并且可能无法在esbuild中添加您想添加的特定功能。这是故意的; esbuild并不意味着它可以满足所有前端需求。