[キャッチアップ] esbuild
もはや覇権を取りそうな Vite が使用している esbuild を知らずに Vite を語れなそうなのでざっくりと理解していこう。
概要
esbuild は超高速なモジュールバンドラ
- キャッシュを使わなくても超早い
- ESM と CJS 両対応
- ESM における Tree shaking
- JS 及び golang 向け API の提供
- TypeScript + JSX 対応
- ソースマップ
- ミニフィケーション
- プラグインサポート
Getting Started
気になるところだけつまみ食いしてく
インストール
プリビルド済みの esbuild のインストール
$ npm install esbuild
バージョンはまだ1未満
$ npx esbuild --version
0.15.9
React アプリをバンドルしてみる
$ npm install react react-dom
import * as React from 'react'
import * as Server from 'react-dom/server'
const Greet = () => <h1>Hello, World</h1>
console.log(Server.renderToString(<Greet />))
バンドル
$ npx esbuild app.jsx --bundle --outfile=out.js
out.js 535.1kb
⚡ Done in 11ms
out.js
に、単体実行可能なバンドルが生成されてる。(React のコードも含まれているため、 node_modules は不要に)
$ node out.js
<h1>Hello, World</h1>
バンドルなしで変換するだけならこう。
.jsx
の場合、デフォルトで JSX も変換されている。
$ npx esbuild app.jsx
import * as React from "react";
import * as Server from "react-dom/server";
const Greet = () => /* @__PURE__ */ React.createElement("h1", null, "Hello, World");
console.log(Server.renderToString(/* @__PURE__ */ React.createElement(Greet, null)));
CLI だけでなく、JavaScript API からも利用可能
require("esbuild")
.build({
entryPoints: ["app.jsx"],
bundle: true,
outfile: "out.js",
})
.catch(() => process.exit(1));
ブラウザ向けビルド
ターゲットブラウザを指定しつつ、ミニファイとソースマップの生成まで行ってみる。
$ npx esbuild app.jsx --bundle --minify --sourcemap --target=chrome58,firefox57,safari11,edge16 --outfile=out.js
out.js 76.0kb
out.js.map 173.2kb
⚡ Done in 6ms
つまり esbuild 自身が、レガシーブラウザ向けの構文変換や、ミニファイ、ソースマップ生成の機能を持っていることがわかる。
Node 向けビルド
Node ならわざわざバンドル生成しなくても動かすことができることがほとんどだけど、 TypeScript のトランスパイルやESM から CommonJS への変換、レガシーNode バージョン向けへの構文変換など、Node でも esbuild が役立つことは多い。
また、パッケージを配布する場合もバンドルのみ公開することで、軽量化することもできる。
const fs = require("fs");
const file = fs.readFileSync("package.json");
console.log(file.toString());
$ npx esbuild app.js --bundle --platform=node --target=node10.4
// app.js
var fs = require("fs");
var file = fs.readFileSync("package.json");
console.log(file.toString());
--platform-node
とすることで、Node のビルトインモジュールについてはバンドルに含めないなどの、パフォーマンス向上のための設定が適用される。
node_modules
に依存していても、あえてバンドルにそれを含めたくない場合は external
オプションが使える。
$ npx esbuild app.jsx --bundle --platform=node --external:./node_modules/*
この場合、 node_modules 以下にある react はバンドルされず、以下のように参照するコードに置き換わるため、ランタイム環境のファイルシステム上に react が存在することを前提とする。
var React = __toESM(require("./node_modules/react/index.js"));
クロスプラットフォーム向けビルド
esbuild は特定OSのためのネイティブコードで書かれているため、プラットフォーム固有のバイナリを他のOSにコピーして動かすことはできない。
通常は package.json
に esbuild
への依存を追加して、各プラットフォームで npm install
することでそのプラットフォームで動くバイナリが手に入るので大した問題にはならない。
この問題に対応する必要があるケースもあって対応方法もあるが、あまり気がのらないので割愛
esbuild はなぜはやいのか
- golang で書かれてコンパイルされたネイティブコードだから
- 多くの主要バンドラが JS で書かれているため遅い
- golang は並列処理に長けている
- JS はスレッド間でデータ共有するためにシリアライズが必要だが、golang ならメモリ共有ができるのでその必要もない
- JS はスレッドごとにヒープが割り当てられるのに対し、golang なら共有できる
- 並列処理を多用している
- esbuild はすべてのCPUを効率的に利用できるように設計されている
- esbuild はすべてが1から再実装されている
- サードパーティライブラリ不使用 (TypeScript parser など)
- すべての処理で一貫したデータ構造を利用することで、変換コストを減らした
- 最適化のために必要であればリアーキテクチャも容易になる
- メモリ効率が非常に良い