【8.4】esbuild(2) - esbuild 原理浅析

2,771 阅读3分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

上一篇文章介绍了开源打包工具 esbuild 的基本介绍和打包速度对比,以及一部分原理知识。这篇文章继续介绍 esbuild 的原理知识,内容来自 esbuild 官网,加上自己的理解和分析,有描述不正确的地方,欢迎讨论指导~

那么,我们先回顾一下,上一篇介绍的 esbuild 内部打包速度优化的四个方面:

    1. esbuild 是用 Go 语言写的,并且编译为 native code
    1. 大量使用并行,充分利用多核 CPU
    1. esbuild 中的所有内容都是从头编写的,没有使用第三方库
    1. 内存得到有效利用

下面我们分别来介绍一下:

1. esbuild 是用 Go 语言写的,并且编译为 native code

其他大多数打包器,因为使用 JavaScript 编写,于是每次编译开始需要先解析打包器的代码,然后再去实际编译 JavaScript 代码,这样就会更慢。而 Go 不属于动态编译的语言,省去了这个步骤。

另外,Go 语言的核心是并行性,JavaScript 不是。Go 可以在线程直接共享内存,JavaScript 需要在线程之间序列化数据。Go 和 JavaScript 都有并行的垃圾收集器,但是 Go 的堆在所有线程间共享,JavaScript 每个线程都有单独的堆。根据测试,这似乎使 JavaScript 的并行性减少了一半,可能是因为 CPU 的内核,一半正在忙着为另一半进行垃圾收集。

2. 大量使用并行

esbuild 内部使用算法保证充分利用多核 CPU,编译过程有三个阶段:解析(parsing)、链接(linking)和代码生成(code generation),解析和代码生成这两个阶段包括了大部分工作,并且可以完全可以并行去做的(大部分情况下,链接是一个串行的任务)。

由于上面提到的,所有线程都可以共享内存。当从不同的入口点,打包相同的 JavaScript 库时,任务可以轻松的被共享。大多数计算机都有多核,于是并行性会是一个非常大的优势。

3. esbuild 中的所有内容都是从头编写的

esbuild 没有使用第三方库,内容都是自己从头编写的,这样会使架构更加的可扩展,并拥有性能优势

举个例子,很多打包工具使用了 Typescript 官网的编译器作为解析器,但是 Typescript 官方的解析器并没有把性能当作一个首要的考虑点。

他们的代码内大量使用了megamorphic object shapes和不必要的dynamic property accesses(这两者会使 JavaScript 的运行速度减慢)

而且在 Typescript 解析器的类型检查被禁用的情况下,貌似还是会执行类型检查。

esbuild 自定义了 Typescript 的解析器

4. 内存得到有效利用

理想情况下,编译器的复杂度时 O(n),所以如果你在处理大量的数据,内存的访问速度可能会严重影响性能。修改数据的次数越少,编译器运行的速度就会越快。

举例来说,esbuild 只涉及三次整个 JavaScript AST

    1. 用于词法分析、解析、作用域设置和符号声明
    1. 绑定符号、最小化语法、把 JSX/TS 编译为 JS、把 ES-next 编译为 ES-2015
    1. 最小化标识符、最小化空格、生成代码和 source map

可以最大程度的重复利用 AST,其他打包器将这些步骤分开进行的,不是交叉进行

以上时 esbuild 官网介绍原理的翻译和理解,有不熟悉或者不当的地方,欢迎评论~

原文参考:esbuild.github.io/faq/#why-is…