Closed8

[キャッチアップ] esbuild

shingo.sasakishingo.sasaki

概要

https://esbuild.github.io/

esbuild は超高速なモジュールバンドラ

  • キャッシュを使わなくても超早い
  • ESM と CJS 両対応
  • ESM における Tree shaking
  • JS 及び golang 向け API の提供
  • TypeScript + JSX 対応
  • ソースマップ
  • ミニフィケーション
  • プラグインサポート
shingo.sasakishingo.sasaki

React アプリをバンドルしてみる

$ npm install react react-dom
app.jsx
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));
shingo.sasakishingo.sasaki

ブラウザ向けビルド

ターゲットブラウザを指定しつつ、ミニファイとソースマップの生成まで行ってみる。

$ 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 自身が、レガシーブラウザ向けの構文変換や、ミニファイ、ソースマップ生成の機能を持っていることがわかる。

shingo.sasakishingo.sasaki

Node 向けビルド

Node ならわざわざバンドル生成しなくても動かすことができることがほとんどだけど、 TypeScript のトランスパイルやESM から CommonJS への変換、レガシーNode バージョン向けへの構文変換など、Node でも esbuild が役立つことは多い。

また、パッケージを配布する場合もバンドルのみ公開することで、軽量化することもできる。

app.js
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"));
shingo.sasakishingo.sasaki

クロスプラットフォーム向けビルド

esbuild は特定OSのためのネイティブコードで書かれているため、プラットフォーム固有のバイナリを他のOSにコピーして動かすことはできない。

通常は package.jsonesbuild への依存を追加して、各プラットフォームで npm install することでそのプラットフォームで動くバイナリが手に入るので大した問題にはならない。

この問題に対応する必要があるケースもあって対応方法もあるが、あまり気がのらないので割愛

shingo.sasakishingo.sasaki

esbuild はなぜはやいのか

https://esbuild.github.io/faq/#why-is-esbuild-fast

  • golang で書かれてコンパイルされたネイティブコードだから
    • 多くの主要バンドラが JS で書かれているため遅い
    • golang は並列処理に長けている
      • JS はスレッド間でデータ共有するためにシリアライズが必要だが、golang ならメモリ共有ができるのでその必要もない
      • JS はスレッドごとにヒープが割り当てられるのに対し、golang なら共有できる
  • 並列処理を多用している
    • esbuild はすべてのCPUを効率的に利用できるように設計されている
  • esbuild はすべてが1から再実装されている
    • サードパーティライブラリ不使用 (TypeScript parser など)
    • すべての処理で一貫したデータ構造を利用することで、変換コストを減らした
    • 最適化のために必要であればリアーキテクチャも容易になる
  • メモリ効率が非常に良い
このスクラップは2022/09/30にクローズされました