🐈

webpackからesbuildへの移行手順(TypeScript)

2022/10/20に公開

TypeScript/ExpressバックエンドをAWSサーバレスにデプロイしています。

バンドルしてツリーシェイキングを効かせるというサイズの削減方法が、Lambdaにデプロイする上で一番ラクなので、もともとwebpackでバンドルしていました。

今回、webpackからesbuildへの乗り換えに成功したため、移行方法をシェアします。

移行編

前提

バンドル対象は以下2点。

  • A. TypeScriptで書かれたExpressのコード
  • B. TypeScriptで書かれたサーバレス関数(DynamoDBへの書き込みによりキックされる)

A.はCLIだけでバンドルできた。

B.は将来、特定フォルダ配下の複数のスクリプトを動的にエントリポイントにしたかったため、JavaScriptAPIを利用した。

修正イメージ

build-sがA.にあたり、build-tがB.にあたる。

移行手順

今回は以下の手順で移行できました。

1. tsconfigの修正

以下3点を修正した。

2. Aのビルドコマンドの修正(CLI)

2-1. ビルド時のエントリポイントについて

esbuildでは、TypeScriptを標準サポートしているため、直接エントリポイントにtsファイルを指定した。
(webpackでもbabel-loaderなど設定すれば可能だが、元はtscでトランスパイルしていた)

2-2. オプションの置き換え

今回の記事のメインです。

A.は、webpack.config.jsを使う代わりにesbuild CLIでのビルドになった。

元のwebpack.config.jsは以下。

...
const server = {
  entry: './dist/path/to/entrypoint.js',
  target: 'node',
  mode: 'production',
  output: {
    path: path.join(__dirname, 'dist/server'),
    filename: 'handler.js',
    libraryTarget: 'commonjs2'
  },
};
...

esbuild CLIに書き直した結果、以下となった。

esbuild ./src/path/to/entrypoint.ts --platform=node --bundle --outfile=./dist/server/handler.ts --target=node14

以下のようにwebpackのプロパティをesbuild CLIの設定に置き換えた。

  • entryプロパティ → esbuildコマンドのパラメータ
  • targetプロパティ → --platformオプション
  • outputプロパティ→ --outfileオプション
  • babel-loader等の実行環境設定→ --targetオプション

その他、今回は動作確認していないが、以下も代替できる模様。

  • productionプロパティによる最適化は--minifyオプション
  • devtoolプロパティによるソースマップ設定は--sourcemapオプション

https://esbuild.github.io/getting-started/#bundling-for-the-browser

2-3. ビルドプロセスの修正

tscを行ってからesbuildするように修正した。

esbuildは型チェックをしないため、TypeScriptの型チェックによるコンパイルエラーの恩恵を受けるためには、tscする必要がある。

(tscが無駄にjsファイルを出力しないため、手順1.でcompilerOptions.noEmitを設定した)

3. Bのビルドスクリプトの修正(JavaScriptAPI)

3-1. ビルドスクリプトの作成

冒頭述べたように、(将来)動的に複数ファイルをエントリポイントとしてビルドできるように、JavaScriptAPIを使って記述。

esbuildのJavaScriptAPIは扱いやすく、async/awaitも用いて以下のようにTypeScriptから扱うことができた。

※移行元のwebpackは割愛

// build-trigger.ts
import { build } from "esbuild";

const triggerNames = ["triggerA", "triggerB", "triggerC"];
const buildPromises = triggerNames.map(async (name) => {
  const sourcePath = `./src/trigger/${name}/handler.ts`;
  const destPath = `./dist/trigger/${name}/handler.js`;
  await build({
    entryPoints: [sourcePath],
    bundle: true,
    outfile: destPath,
    platform: "node",
    target: "node14",
  });
});

Promise.all(buildPromises)
  .then(() => {
    console.log("done trigger build");
  })
  .catch((reason) => {
    console.log(reason);
  });

3-2. ビルドプロセスの修正

手順2-3. 同様、tscコマンドをesbuildの前に実行するように修正。

採用編

今回移行して感じた利点

1. ビルド時間の短縮

ビルドが速くなり、快適。CLIもJavaScript APIも速い。

webpackで10秒以上かかっていた処理が、1秒未満になった。

2. デベロッパーフレンドリ

  • tsc標準対応
  • CLIでできる範囲が広くわかりやすい
  • JavascriptAPI自体がTypeScript対応しており、asyn/awaitにも対応(かつ、並行で動かしても速い)

など、開発者の手に馴染む印象があった。

esbuild採用にあたっての注意点

1. 開発途上の機能について

async chunkのコード分割、CSSモジュールのサポートなどが未対応。
フロントエンドをバンドルする上では要注意。

https://esbuild.github.io/faq/#upcoming-roadmap

2. TypeScriptプロジェクトへの適用

esbuildにビルドさせる以上、tsconfigのコンパイラに関する設定が一部しか利かなく点は特に注意。

https://esbuild.github.io/content-types/#typescript-caveats

3. 本番採用について

(2022年7月時点)バージョンは1.0.0になっていないものの、概ね安定しているため、採用可否は使う側のポリシー次第とされている。

実際にviteなどの有名ライブラリによる採用実績もある。

https://esbuild.github.io/faq/#production-readiness

追伸1

npm installeslintprettiertscなどの時間もあるため、esbuildが10倍速いから10倍速、というわけにはいきませんでした。

それでも、esbuildを採用し、全体で20秒以上かかっていたバックエンドのビルドプロセスが10秒以内になったことで、体感上とても快適に感じています。

個人的には、ミッションクリティカルなアプリケーションでなければ、本番導入していきたいです。(CIやテスト環境もesbuildでビルドしている前提)

追伸2

この記事はMagicodeから移転しました。
2022年8月あたりの記事となります。

よければTwitterもフォローお願いします!
@sumiren_t

Discussion