浅谈ESBuild & SWC

3,151 阅读9分钟

前言

随着项目体积的增加,webpack的打包速度也开始趋于瓶颈。特别是在开发阶段,少量的代码更新,都需要等一段时间才能编译完成,这样严重影响开发体验和速度。最近也一直在研究webpack4webpack5的升级,在打包速度上有所提升,打包体积上也有所减少(对tree-shaking的优化)。之前也听到有同学分享了关于vite在开发阶段的打包速度较快,主要是用到了ESBuild,于是调研了一下ESBuildSWC的打包编译原理,跟webpack做一下对比。

什么是ESBuild & SWC

  • ESBuild是基于Go语言开发的JavaScript Bundler, 由Figma前CTO Evan Wallace开发, 并且也被Vite用于开发环境的依赖解析和Transform。
  • SWC则是基于Rust的JavaScript Compiler(其生态中也包含打包工具spack), 目前为Next.JS/Parcel/Deno等前端圈知名项目使用。

从这个解释可以简单的理解ESBuild是一个打包工具,类似于webpack,SWC主要是一个编译工具,类似于babel。

调研ESBuild & SWC的目的

47f2a55919c493f5cfc062c85879a08d.jpeg

  • 前边提到随着项目体积变大,每次重新编译都需要等很久,那么我们先看下ESBuild的速度:

esbuild.png

此图是ESBuild官网首页展示的打包十份three.js的速度各个打包工具的速度对比。可以明显的看出速度的差异。
点击查看

  • SWC则宣称其比Babel快20倍(四核情况下可以快70倍)

swc.png

  那么ESBuild和SWC的速度是否真的像它们宣传的这么快吗,接下来我们就实际打包一下,看下具体的速度是否和它们的开发者所说的一样。
首先我们用ESBuild的官网的例子分别用ESBuild和webpack打包对比一下速度。

import React from 'react';
import Server from 'react-dom/server';

const Greet = () => <h1>Hello, world!</h1>;

console.log(Server.renderToString(<Greet />));
  • ESBuild打包

esbuildb.png

  • webpack打包

wpbuild.png

  接下来我们再测试一下SWC和babel的编译速度,测试用如下例子es6.js。

// 声明一些变量
const PI = Math.PI;
let x = 1;

// spread
let [foo, [[bar], baz]] = [1, [[2], 3]];
const node = {
  loc: {
    start: {
      line: 1,
      column: 5,
    },
  },
};
let {
  loc,
  loc: { start },
  loc: {
    start: { line },
  },
} = node;

const color = ['red', 'yellow'];
const colorful = [...color, 'green', 'pink'];

const first = {
  a: 1,
  b: 2,
  c: 6,
};
const second = {
  c: 3,
  d: 4,
};
const total = { ...first, ...second };

// arrow function
var people = (name, age) => {
  return {
    name,
    age,
  };
};

// set
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach((x) => s.add(x));

// class
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return `(${this.x}, ${this.y})`;
  }
}

// Generators
function* createIterator() {
  yield 1;
  yield 2;
  yield 3;
}

// 生成器能像正规函数那样被调用,但会返回一个迭代器
let iterator = createIterator();
  • babel编译

babel.png

  • SWC编译

swct.png

ESBuildwebpack
0.74s3.60s
SWCbabel
0.37s1.50s

  通过上面的的打包编译速度对比可以看出ESBuild的打包速度比webpack要快,SWC的编译速度也比babel块。另外说明一点,上面的对比和测试用例都是用的相对比较简单的,并且在对比中也没有充分使用各个构建工具所有的构建优化策略, 只是对比最基础的配置下几种工具的速度, 这个和各个工具官网所罗列的数据会有差异, 并且构建速度也和硬件性能/运行时状态有关。如果大家有兴趣了可以尝试一下官网的例子。
虽然ESBuild & SWC速度这么快,但是目前还是没法完全替代webpack & babel。那么是否可以为我们所用呢,请继续往下看。

ESBuild & SWC在前端生态圈的定位

  • 在当今的前端生态圈里,各种工具层出不穷,接下来我们就看下ESBuild & SWC具体在生态圈里担任着什么角色。

前端图谱.png

  • 从上图中我们选几个日常接触最频繁的前端工程化工具:

    • Loader:前端项目中包含各种文件类型和数据, 需要将其进行相应的转换变成JS模块才能为打包工具使用并进行构建. JS的Compiler和其他类型文件的Loader可以统称为Transfomer.
    • Plugin:可以更一步定制化构建流程, 对模块进行改造(比如压缩JS的Terser)
    • 还有一些前端构建工具是基于通用构建工具进行了一定封装或者增加额外功能的, 比如Vite/Umi
    • Task Runner任务运行器:开发者设置脚本让构建工具完成开发、构建、部署中的一系列任务, 大家日常常用的是npm/yarn的脚本功能; 在更早一些时候, 比较流行Gulp/Grunt这样的工具
    • Package Manager包管理器:这个大家都不会陌生, npm/Yarn/pnmp帮开发者下载并管理好依赖, 对于现在的前端开发来说必不可少.
    • Compiler/Transpiler编译器: 在市场上很多浏览器还只支持ES5语法的时候, Babel这样的Comipler在前端开发中必不可少; 如果你是用TypeScript的话, 也需要通过tsc或者ts-loader进行编译.
    • Bundler打包工具:从开发者设置的入口出发, 分析模块依赖, 加载并将各类资源最终打包成1个或多个文件的工具.
  • ESBuild的定位是Bundler,但是也是Compiler(有Transform代码的能力);

  • SWC自称其定位为Compiler,主要对标的是babel, 但是也具有简单的打包能力,目前swc的Bundle工具叫spack, 后续会改名为swcpack。(我们简单测试一下):

    • 添加index.js和utils/log.js
// index.js
import { log } from './utils/log';
const start = () => log('app started');
start();

// log.js
export const log = function () {
  console.log(...arguments);
};

export const errorLog = function () {
  console.error(...arguments);
};
  • 添加index.js和utils/log.js
module.exports = {
  entry: {
    // 打包的入口
    web: __dirname + '/src/index.js',
  },
  output: {
    // 打包后输出的文件夹
    path: __dirname + '/dist',
  },
};
  • 执行命令
yarn spack
  • 打包后的文件
var log = function log() {
    var _console;
    (_console = console).log.apply(_console, arguments);
};
var start = function() {
    return log("app started");
};
start();

可以看到swc可以打包,同时也具有tree-shaking能力,但是目前spack还不够完善,无法做到开箱即用。(无法打包简单的react应用)

ESBuild & SWC(速度) 为何大于 webpack & babel(速度)

  • 首先简单看下各自的区别:
工具开发语言执行方式
ESBuildGo新开一进程,然后多线程并行
webpackjs单线程串行
SWCRust官网暂未明确
babeljs单线程串行
  • ESBuild的实现(参考ESBuild FAQ(常见问题))

    • 由Go实现并编译成本地代码:  多数Bundler都是由JavaScript实现的, 但是CLI应用对于JIT编译语言来说是性能表现最不好的。每次运行Bundler的时候, JS虚拟机都是以第一次运行代码的视角来解析Bundler(比如Webpack)的代码, 没有优化信息. 当ESBuild在解析JavaScript的时候, Node还在解析Bundler的JS代码
    • 重度使用并行计算:  Go语言本身的设计就很重视并行计算, 所以ESBuild对这一点会加以利用. 在构建中主要有三个环节: 解析(Parsing), 链接(Linking)和代码生成(Code generation), 在解析和代码生成环节会尽可能使用多核进行并行计算
    • ESBuild 中的一切代码从零实现:  通过自行实现所有逻辑来避免第三方库带来的性能问题, 统一的数据结构可以减少数据转换开销, 并且可以根据需要改变架构, 当然最大的缺点就是工作量倍增
    • 对内存的高效使用:  ESBuild在实现时尽量减少数据的传递以及数据的转换, ESBuild尽量减少了对整体AST的传递, 并且尽可能复用AST数据, 其他的Bundler可能会在编译的不同阶段往复转换数据格式(string -> TS -> JS -> older JS -> string…). 在内存存储效率方面Go也比JavaScript更高效
  • SWC的实现

    • swc的官方文档和网站并没有对swc内部实现的较为具体的解释, 根据其博客中的一些分析, babel缓慢的主要原因还是来自于其单线程的特性

ESBuild & SWC如何为我们所用

使用ESBuild

  • 如果单独使用ESBuild的话,还是有一些限制能力的,比如:

    • 没有 TS 类型检查
    • 不能操作 AST
    • 不支持装饰器语法
    • 产物 target 无法降级到 ES5 及以下

可以在一些小型项目里面尝试。

  • 可以配合webpack使用,在webpack体系中使用ESBuild的loader来替代babel用于进行代码转换, 除此之外, esbuild-loader还可以用于JS & CSS的代码最小化,由于不支持es5,所以可以暂时在开发阶段使用,加快本地打包速度,具体的使用方式还在调研中…
const { ESBuildMinifyPlugin } = require('esbuild-loader')

module.exports = {
    rules: [
      {
        test: /.js$/,
        // 使用esbuild作为js/ts/jsx/tsx loader
        loader: 'esbuild-loader',
        options: {
          loader: 'jsx',  
          target: 'es2015'
        }
      },
    ],
    // 或者使用esbuild-loader作为JS压缩工具
    optimization: {
      minimizer: [
        new ESBuildMinifyPlugin({
          target: 'es2015'
        })
      ]
    }
}
  • 也可以尝试使用Vite

    • Vite的核心理念是使用ESM+编译语言工具(ESBuild)加快本地运行,
    • Vite在开发环境使用了ESBuild进行预构建, 在生产环境使用了Rollup打包, 后续也有可能使用ESBuild进行生产环境的构建。
    • 具体使用方式可以参考Vite和之前,丁林分享的这篇文章Vite 原理浅析及应用

使用swc

  • SWC 与 babel 一样,将命令行工具、编译核心模块分化为两个包。

    • @swc/cli 类似于 @babel/cli
    • @swc/core 类似于 @babel/core
  • SWC的核心部分swc/core主要有三种API

    • Transform: 代码转换API, 输入源代码 => 输出转换后的代码
    • Parse: 对源代码进行解析, 输出AST
    • Minify: 对代码进行最小化
    • 编译后的产物支持es5
  • 与webpack结合使用,用SWC替换babel,也可以使用swc-loader

module: {
    rules: [
      {
        test: /.(js|jsx|ts|tsx)$/,
        include: path.resolve(__dirname, '../demo'),
        use: [
          {
            loader: 'swc-loader',
            options: {
              cacheDirectory: true,
            },
          },
        ],
      },
    ],
  },
  • 配置文件(swc 与 babel 一样,支持类似于 .babelrc 的配置文件:.swcrc,配置的格式为 JSON)
{
  "jsc": { // 编译规则
    "target": "es5", // 输出js的规范
    "parser": {
      // 除了 ecmascript,还支持 typescript
      "syntax": "ecmascript",
      // 是否解析jsx,对应插件 @babel/plugin-transform-react-jsx
      "jsx": false,
      // 是否支持装饰器,对应插件 @babel/plugin-syntax-decorators
      "decorators": false,
      // 是否支持动态导入,对应插件 @babel/plugin-syntax-dynamic-import
      "dynamicImport": false,
      // ……
      // babel 的大部分插件都能在这里找到对应配置
    },
    "minify": {}, // 压缩相关配置,需要先开启压缩
  },
  "env": { // 编译结果相关配置
    "targets": { // 编译结果需要适配的浏览器
      "ie": "11" // 只兼容到 ie 11
    },
    "corejs": "3" // corejs 的版本
  },
  "minify": true // 是否开启压缩
}
  • Bundling (swcpack),前边也提到了SWC的打包能力还不是很完善,主要功能还是用在Transform上,期待后续的完善。

swcbbb.png

总结

  • ESBuild和SWC是使用编译型语言开发的新一代前端工具,相比于用js开发的构建工具有系统级的速度优势
  • 目前这两个工具在打包和编译方面还处于社会主义初级阶段,生态圈还相对薄弱,无法做到开箱即用
  • 目前还无法完全替代webpack和babel完全应用到当前项目中
  • 可以尝试使用他们的部分功能来与webpack进行结合,为现有项目的开发提升速度,从而提升开发效率和体验(继续研究中)
  • 持续关注前端生态圈的发展,从中选择出能够提升我们开发效率的工具

参考资料