ESBuild & SWC解析: 距离新一代构建、打包工具有多远?

大家好,很高兴又见面了,我是"web 前端分享",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1.ESBuild & swc是什么?

ESBuild:是基于Go语言开发的JavaScript Bundler, 由Figma前CTO Evan Wallace开发, 并且也被Vite用于开发环境的依赖解析和Transform。

SWC:是基于Rust的JavaScript Compiler(其生态中也包含打包工具spack),目前为Next.JS/Parcel/Deno等前端圈知名项目使用。

2.为什么要关注这两个工具?

大家可能在日常工作中遇到过, 项目的构建时间随着项目体积和复杂度逐渐递增, 有的时候本地编辑一个项目要等上个大几分钟(此处@Webpack)

这个是ESBuild官网对于其打包10份three.js的速度对比

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

那么ESBuild & SWC是真的有这么快? 还是开发者的自说自话? 我们通过实验来检验一下。 先看ESBuild,让我们先写一段非常简单的代码

import * as React from 'react'
import * as ReactServer from 'react-dom/server'
const Greet = () => 

Hello, world!

console.log(ReactServer.renderToString())

然后我们来通过Webpack & ESBuild构建它。用ESBuild打包一下

//编译
> build-esb
> esbuild ./src/app.jsx --bundle --outfile=out_esb.js --minify

//构建产物的大小和构建时间
out_esb.js  27.4kb
⚡ Done in 13ms

//运行产物
node out_esb.js 

Hello, world!

用Webpack打包一下

//编译
> build-wp
> webpack --mode=production

//构建产物
asset out_webpack.js 25.9 KiB [compared for emit] [minimized] (name: main) 1 related asset
modules by path ./node_modules/react/ 8.5 KiB
  ./node_modules/react/index.js 189 bytes [built] [code generated]
  ./node_modules/react/cjs/react.production.min.js 8.32 KiB [built] [code generated]
modules by path ./node_modules/react-dom/ 28.2 KiB
  ./node_modules/react-dom/server.browser.js 227 bytes [built] [code generated]
  ./node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 28 KiB [built] [code generated]
./src/app.jsx 254 bytes [built] [code generated]
./node_modules/object-assign/index.js 2.17 KiB [built] [code generated]

//构建时间
webpack 5.72.0 compiled successfully in 1680 ms

npm run build-wp  2.79s user 0.61s system 84% cpu 4.033 total

//运行
node out_webpack.js  

Hello, world!

// 一些变量声明
const PI = 3.1415;
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;

// arrow function
var sum = (num1, num2) => { return num1 + num2; }

// 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 + ')';
  }
}
yarn compile-babel
yarn run v1.16.0
warning package.json: No license field
$ babel src/es6.js -o es6_babel.js
✨  Done in 2.38s.
yarn compile-swc  
yarn run v1.16.0
warning package.json: No license field
$ swc src/es6.js -o es6_swc.js
Successfully compiled 1 file with swc.
✨  Done in 0.63s.
// es6_babel
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
var PI = 3.1415;
var x = 1;
var foo = 1,
    bar = 2,
    baz = 3;
var node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};
var loc = node.loc,
    start = node.loc.start,
    line = node.loc.start.line;

var sum = function sum(num1, num2) {
 return num1 + num2;
};

var s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(function (x) {
 return s.add(x);
});

var Point = /*#__PURE__*/function () {
 function Point(x, y) {
    _classCallCheck(this, Point);

    this.x = x;
    this.y = y;
  }

  _createClass(Point, [{
    key: "toString",
    value: function toString() {
 return '(' + this.x + ', ' + this.y + ')';
    }
  }]);

 return Point;
}();

// es6 swc
function _classCallCheck(instance, Constructor) {
 if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
function _defineProperties(target, props) {
 for(var i = 0; i < props.length; i++){
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
 if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
    }
}
function _createClass(Constructor, protoProps, staticProps) {
 if (protoProps) _defineProperties(Constructor.prototype, protoProps);
 if (staticProps) _defineProperties(Constructor, staticProps);
 return Constructor;
}
var PI = 3.1415;
var x = 1;
var foo = 1, bar = 2, baz = 3;
var node = {
    loc: {
        start: {
            line: 1,
            column: 5
        }
    }
};
var loc = node.loc, start = node.loc.start, _loc = node.loc, line = _loc.start.line;
var sum = function(num1, num2) {
 return num1 + num2;
};
var s = new Set();
[2,3,5,4,5,2,2].forEach(function(x1) {
 return s.add(x1);
});
var Point = /*#__PURE__*/ function() {
 "use strict";
 function Point(x2, y) {
        _classCallCheck(this, Point);
        this.x = x2;
        this.y = y;
    }
    _createClass(Point, [
        {
            key: "toString",
            value: function toString() {
 return "(" + this.x + ", " + this.y + ")";
            }
        }
    ]);
 return Point;
}();

//# sourceMappingURL=es6_swc.js.map

从上面的数据可以看出

在打包代码的对比, ESBuild的速度(20ms)远快于Webpack(1680ms)

在编译代码的对比, swc也对babel有比较明显的性能优势(0.63s vs 2.38s).

需要额外说明的是, 用作实例的代码非常简单, 并且在对比中也没有充分使用各个构建工具所有的构建优化策略, 只是对比最基础的配置下几种工具的速度, 这个和各个工具所罗列的benchmark数据会有差异, 并且构建速度也和硬件性能/运行时状态有关.

ESBuild/swc这么快? 那是不是可以直接把Webpack/Babel扔掉了? 也别急, 目前的ESBuild和Swc可能还不能完全替代Webpack。但是通过这篇分享我们也许可以对它们有一个更全面的认知, 也可以探索后边在工作中使用这些新一代前端工具的机会。

3.ESBuild/swc在前端生态中的定位

在当今的前端世界里, 新工具层出不穷, 有的时候不同的工具太多以至于有段时间我完全分不清这些工具各自的功能是什么, 所以我们先来研究一下ESBuild/swc在当今前端工程体系中的角色。

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

ESBuild的定位是Bundler, 但是它也是Compiler(有Transform代码的能力)

swc自称其定位为Compiler + Bundler, 但是目前spack还不是很好用

4. ESBuild/SWC为何这么快?

思考一下, Go & Rust这两个语言和JavaScript相比有什么差异?

4.1 ESBuild的实现

令人想到了SpaceX这家公司, 大量零部件都是自己内部生产, 有效降低生产成本


4.2 swc的实现

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

一点总结

从ESBuild和swc的官方资源中, 共同提到的一点就是利用好并行计算。JS因为在设计之初的目标就是服务好浏览器场景, 所以单线程 & 事件驱动并不适合用来进行CPU密集的计算, 而ESBuild/Rust也正是在这一点上对基于Node的构建工具拥有系统性的速度优势。

如何用ESBuild/swc提效?

现在我们知道ESBuild/Rust是做什么的, 并且有什么特点, 我们可以在工作中如何利用ESBuild/swc去改善我们的开发体验呢?

使用ESBuild

ESBuild在API层面上非常简洁, 主要的API只有两个: Transform和Build, 这两个API可以通过CLI, JavaScript, Go的方式调用。Transform主要用于对源代码的转换, 接受的输入是字符串, 输出的是转换后的代码

//用CLI方式调用, 将ts代码转化为js代码
echo 'let x: number = 1' | esbuild --loader=ts => let x = 1;
// 用JS模式调用build方法
require('esbuild').buildSync({
  entryPoints: ['in.js'],
  bundle: true,
  outfile: 'out.js',
})
require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.js': 'jsx' },
  outfile: 'out.js',
})
// 来自于官网的插件示范
let envPlugin = {
  name: 'env',
  setup(build) {
    // Intercept import paths called "env" so esbuild doesn't attempt
    // to map them to a file system location. Tag them with the "env-ns"
    // namespace to reserve them for this plugin.
    build.onResolve({ filter: /^env$/ }, args => ({
      path: args.path,
      namespace: 'env-ns',
    }))
    // Load paths tagged with the "env-ns" namespace and behave as if
    // they point to a JSON file containing the environment variables.
    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))

在其他工具中使用ESBuild

如果你觉得目前完全使用ESBuild还不成熟, 也可以在Webpack体系中使用ESBuild的loader来替代babel用于进行代码转换, 除此之外, esbuild-loader还可以用于JS & CSS的代码最小化.

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'
        })
      ]
    }
}

注意点:ESBuild不能转ES5代码和一些其他语法, 详情可参考https://esbuild.github.io/content-types/#javascript-caveats

使用Vite

要说2021年前端圈关注度较高的新工具, Vite可以说是名列前茅, 那么Vite和ESBuild/swc有什么关系呢?

Vite的核心理念是使用ESM + 编译语言工具ESBuild加快本地运行

Vite在开发环境使用了ESBuild进行预构建, 在生产环境使用了Rollup打包, 后续也有可能使用ESBuild进行生产环境的构建。

支持ES5需要引入插件 https://github.com/vitejs/vite/tree/main/packages/plugin-legacy

使用swc

Comilation

可以使用swc命令行工具(swc/cli)配合配置文件对文件进行编译

// Transpile one file and emit to stdout
npx swc ./file.j
//Transpile one file and emit to `output.js`
npx swc ./file.js -o output.js
//Transpile and write to /output dir
npx swc ./my-dir -d output

Transform: 代码转换API, 输入源代码 => 输出转换后的代码

Parse: 对源代码进行解析, 输出AST

Minify: 对代码进行最小化

swc也推出了swc/wasm模块, 可以让用户在浏览器环境使用wasm进行代码转换。如果你想在Webpack体系下使用swc(替代babel), 也可以使用swc-loader。

Bundle

⚠️swc也支持进行打包功能, 但是目前功能还不很完备, 并且在使用中也有不少Bug。笔者目前在本地尝试用spack打包一个简单的React应用目前还不成功, 还做不到开箱即用


目前swc的Bundle工具叫spack, 后续会改名为swcpack。打包可以通过spack.config.js文件进行配置

6.总结和思考

全文总结

1. ESBuild/swc是用编译型语言编写的新一代前端工具, 对JS编写的构建工具有系统级的速度优势

2. ESBuild可以用于编译JS代码和模块打包, swc号称也都可以支持两者但是其打包工具还处于早期开发阶段

3. 目前这两个工具还不能完全替代Webpack等主流工具这些年发展出的庞大生态

4. 当已有的基础设施稳定并且替换成本较大时, 可以尝试渐进式的利用新工具(loader)或者Vite这种基于ESBuild二次封装的构建工具

延伸思考

1. 持续关注前端生态新发展, 利用好开源社区提升研发效率和体验的新工具.

2. 在使用新工具的同时, 了解或参与到其背后的技术原理, Go可以作为服务端语言, Rust可以作为系统编程语言, 学习新语言能打开新天地, 岂不美哉?

参考资料

原文链接:https://zhuanlan.zhihu.com/p/506855608

展开阅读全文

页面更新:2024-04-13

标签:工具   最小化   新一代   产物   源代码   插件   距离   语言   代码   环境   文件   项目

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top