Esbuild 为何如此之快?

天下武功,唯快不破~

前言

关注 Vite 底层实现的同学,我想应该清楚它使用esbuild来实现对 .tsjsx.js 代码的转化。当然,在 Vite 之前更早使用 esbuild 的就是Snowpack。不过,相比较 Vite 拥有的巨大社区,显然Snowpack的关注度较小。

Vite 的核心是基于浏览器原生的 ES Module。但是,相比较传统的打包工具和开发工具而言,它做出了很多改变,采用 esbuild 来支持 .tsjsx.js 代码的转化就是其中之一。

1 什么是 esbuild

esbuild官方的介绍:它是一个JavaScript Bundler 打包和压缩工具,它可以将JavaScriptTypeScript代码打包分发在网页上运行。

目前 esbuild 支持的功能:

  • 支持 js、ts、jsx、css、json、文本、图片等资源
  • 增量更新
  • Source map 生成
  • 开发服务器支持
  • 代码打包和压缩
  • Code split
  • Tree shaking
  • 插件支持
  • ...

这里,我们列出了几点常关注的,至于其他,有兴趣的同学可以移步官方文档自行了解。

目前对于JavaScript语法转化不支持的特性有:

  • Top-level await
  • async await
  • BigInt
  • Hashbang 语法

需要注意的是对于不支持转化的语法会原样输出

2 对比现有的打包工具

esbuild 的作者对比目前现阶段类似的工具做了基准测试。最后的结果是:

对于这些基准测试,esbuild 比其他 JavaScript 打包程序 快至少 100 倍。

100 倍,可以说快到飞起了...而 esbuild 快的原因,这里我分两个层面解释:

2.1 官方解释

  • 它是用 Go 语言编写的,该语言可以编译为本地代码。
  • 解析,生成最终文件和生成 source maps 全部完全并行化。
  • 无需昂贵的数据转换,只需很少的几步即可完成所有操作。
  • 该库以提高编译速度为编写代码时的第一原则,并尽量避免不必要的内存分配。

2.2 语言层面解释

  • 现阶段的类似工具,底层的实现都是基于JavaScript,其受限于本身是一门动态解释型的语言,每次执行都需要先由解释器一边将源码翻译成机器语言,一边穿插调度执行,并不能充分利用 CPU。
  • 「Chrome V8」引擎虽然对JavaScript的运行做了优化,引进「JIT」(即时编译)的机制,但是部分代码实现机器码与 esbuild 全部实现机器码的形式,性能上的差距不可弥补。

当然,语言层面仅仅是官方解释中的一点的展开,其他解释有时间等后续分析其源码实现后讲解。

3 esbuild API 详解

虽然, esbuild 早已开源和使用,但是官方文档只是简单介绍了如何使用,而对于 API 介绍部分是欠缺的,建议读者自己去阅读源码中的定义。

esbuild 总共提供了八个函数:transformbuildbuildSyncformatMessagesformatMessagesSyncinitializeservetransformtransformSync

我们可以打印出来看看

js 复制代码
const esbuild = require("esbuild");
console.log("build esbuild", esbuild);

下面挑几个主要的方法,其他方案的定义可以自行前往官方文档了解,从源码定义的角度来认识一下它们。

3.1 transform

transform 可以用于转化 .js.tsxts 等文件,然后输出为旧的语法的 .js 文件,它提供了两个参数:

  • 第一个参数(必填,字符串),指需要转化的代码(模块内容)。
  • 第二个参数(可选),指转化需要的选项,如源文件路径 sourcefile、需要加载的 loader,其中 loader 的定义:
ini 复制代码
type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'css' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary';

transform 会返回一个 Promise,对应的 TransformResult 为一个对象,它会包含转化后的旧的 js 代码、sourceMap 映射、警告信息:

ini 复制代码
interface TransformResult {
  js: string;
  jsSourceMap: string;
  warnings: Message[];
}

3.2 build

build 实现了 transform 的能力,即代码转化,并且它还会将转换后的代码压缩并生成 .js 文件到指定 output 目录。build 只提供了一个参数(对象),来指定需要转化的入口文件、输出文件、loader 等选项:

js 复制代码
interface BuildOptions extends CommonOptions {
  bundle?: boolean;
  splitting?: boolean;
  outfile?: string;
  metafile?: string;
  outdir?: string;
  platform?: Platform;
  color?: boolean;
  external?: string[];
  loader?: { [ext: string]: Loader };
  resolveExtensions?: string[];
  mainFields?: string[];
  write?: boolean;
  tsconfig?: string;
  outExtension?: { [ext: string]: string };

  entryPoints?: string[];
  stdin?: StdinOptions;
}

build 函数调用会输出 BuildResult,它包含了生成的文件 outputFiles 和提示信息 warnings

js 复制代码
interface BuildResult {
  warnings: Message[];
  outputFiles?: OutputFile[];
}

但是,需要注意的是 outputFiles 只有在 writefalse 的情况下才会输出,它是一个 Uint8Array

3.3 buildSync

buidSync 顾名思义,相比较 build 而言,它是同步的构建方式,即如果使用 build 我们需要借助 async await 来实现同步调用,而使用 buildSync 可以直接实现同步调用。

4 实现一个小而美的 Bundler 打包

在简单地认识 esbuild ,我们就来实现一个小而美的 Bunder 打包:

4.1 初始化项目和安装 esbuild

bash 复制代码
mkdir esbuild-bundler && cd ./esbuild-bundler && npm init -y && npm i esbuild lodash-es

4.2 目录结构:

bash 复制代码
|--------- src
     |------ main.js  #项目入口文件
|--------- index.js     #bundler实现核心文件
|--------- package.json     #npm包配置

4.3 index.js

js 复制代码
const buildHandler = async () => {
  const esbuild = require("esbuild");
  try {
    const result = await esbuild.build({
      entryPoints: ["./src/main.js"],
      outfile: "./dist/main.js",
      minify: true, // 是否压缩代码
      bundle: true, // 是否打包成代码块
    });
    console.log("build result", result);
  } catch (error) {
    console.error(`esbuild build error`, error);
  } finally {
    console.log('esbuild finally');
  }
};
buildHandler();

4.4 ./src/main.js

js 复制代码
import { cloneDeep } from 'lodash-es';
const obj = {
  name: 'james',
  like: {
      drink: 'pure water',
      game: 'wangzhe' 
  },
  age: 18
};
const obj2 = cloneDeep(obj);
obj.age = 20;
obj.like.drink = 'Cola';
console.log('obj', obj);
console.log('obj2', obj2);

4.5 package.json

json 复制代码
{
  "name": "esbuild-bundler",
  "version": "1.0.0",
  "description": "",
  "main": "./src/main.js",
  "scripts": {
    "build": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "esbuild": "^0.12.15",
    "lodash-es": "^4.17.21"
  }
}

4.6 运行看结果

执行一下 npm run build, 看下 闪电般的 bundler 打包结果!

打包后的代码如下:

执行看下输出结果:

总结

现在大家对 esbuild 应该可以建立起一个基础的认知。不过按照目前的社区使用情况来看,esbuild 暂时不可能替代 webpack、parcel 等构建工具,因为他们做的事情是不太一样,插件体系没有前面几个老大哥那么丰富,它更适合作为一种偏底层的模块打包工具,然后需要在它的基础上进行二次封装,扩展出一套既能兼顾性能,又有完善工程化能力的工具链,例如 Snowpack, Vite, SvelteKit, Remix Run等,而且,不过基于编译性能层面来说,esbuild 还是遥遥领先的,可以说是完全为性能而定制的。

相关推荐
王解2 天前
速度革命:esbuild如何改变前端构建游戏 (1)
前端·vite·esbuild
景天科技苑3 天前
【vue3+vite】新一代vue脚手架工具vite,助力前端开发更快捷更高效
前端·javascript·vue.js·vite·vue项目·脚手架工具
niech_cn6 天前
vite + vue3 + ts解决别名引用@/api/user报错找不到相应的模块
vite
Amd7947 天前
Nuxt.js 应用中的 vite:compiled 事件钩子
自定义·vite·编译·nuxt·热更新·性能·钩子
黑色的糖果7 天前
npm上传自己封装的插件(vue+vite)
前端·vue.js·npm·vite
软件小伟8 天前
Vite是什么?Vite如何使用?相比于Vue CLI的区别是什么?(一篇文章帮你搞定!)
前端·vue.js·ecmascript·vite·vue vli
Amd7948 天前
Nuxt.js 应用中的 vite:serverCreated 事件钩子
中间件·开发·vite·日志·nuxt·跨域·钩子
亦世凡华、8 天前
React--》如何高效管理前端环境变量:开发与生产环境配置详解
react·vite·环境变量·env·env配置
19组清风9 天前
对于模块动态加载,Vite 内部做了哪些优化
前端·vite·前端工程化
Amd7949 天前
Nuxt.js 应用中的 vite:configResolved 事件钩子
vite·配置·nuxt·构建·钩子·动态·调整