esbuild で Node.js を TypeScript 化する
TLDR
- バックエンドのコードを esbuild で TypeScript 化した
- ビルド時間は1秒ほど
- tsc に比べて10倍以上高速化できた
- 特に問題なくプロダクションで安定稼働している
esbuild はいつ使えるのか
みなさん webpack は使っていますか?まだまだ広く使われていると思いますが、最近では esbuild の事例もたまに見るようになってきました。
- ABEMAにesbuildを導入してWebのバンドル処理を69倍高速化した話 | CyberAgent Developers Blog
- esbuild が爆速すぎて webpack / Rollup にはもう戻れない | Kabuku Developers Blog
現時点で esbuild をフロントエンドの Bundler として導入するには、以下のような問題があります。
- ES5にトランスパイルできない
- Code Splitting ができない
- CSS Modules に対応していない
- Module Federation に対応しない
上記を考慮すると、プロダクション環境で使うのは難しいサービスが多いのが現実だと思います。また、Next.js などのフレームワークを使っている場合、基本的には webpack にべったりなので、導入しにくいです (esbuild-loader などはありますが)。
しかし、ビルド対象がフロントエンドではなくバックエンドのコードであればどうでしょうか。
ここでいうバックエンドとは、サーバーで動くNode.jsアプリケーションを指しています。サーバーのみで動くコードであれば、前述した問題はどれも関係なさそうに見えます。また、esbuild の issue を1-2ヶ月ほど追ってみましたが、純粋な TypeScript のパーサー周りは非常に安定していると感じました。
上記から、バックエンドのコードに限れば esbuild を導入できるのでは?と考えました。今回は、Node.jsのサーバーサイドアプリケーションを TypeScript 化するのに esbuild を使ってみたので、その様子を書いてみようと思います。
esbuild でTS化する
開発環境
開発環境では、ts-node の代替として esbuild を使用します。今回は、esbuild-registerというラッパーを使用しました。
使い方は簡単で、起動コマンドを以下のようにするだけです。
node -r esbuild-register app.ts
実際にこれだけで開発環境で TypeScript を使うことができるようになりました。
本番環境
本番環境では、tsc コマンドの代替として esbuild を使用します。開発環境とはまた別のラッパーなのですが、esbuild-node-tscを使用しています(このあたりのことは後述します)。
Transpile 自体は tsconfig さえあれば以下のコマンドで実行できます。
etsc
Transpile に加えて、アセットファイルのコピーも設定できるので、以下のように設定ファイルも加えます。node_modules
を除外しないとコピーするアセットのトラバースで時間がかかってしまうので注意です。
module.exports = {
tsConfigFile: 'tsconfig.node.json',
assets: {
baseDir: '.',
filePatterns: [
// 以下の拡張子の assets をコピー
'**/*.{json,yaml}',
// 不要なので除外
'!node_modules',
// その他不要なファイルは適宜除外
],
},
};
実際のビルドはイメージの生成時に行いました。いろいろ省いていますが、以下のような雰囲気でマルチステージビルドしています。
# ------------------------------
# builder
# ------------------------------
FROM node:16-alpine AS builder
RUN yarn install --frozen-lockfile --production=false && \
# ここで TS をビルドする
yarn build && \
npm prune --production && \
yarn cache clean
# ------------------------------
# production image
# ------------------------------
FROM node:16-alpine AS production
EXPOSE 3000
WORKDIR /home/node/app
COPY /home/node/app/dist .
COPY /home/node/app/node_modules ./node_modules
COPY /home/node/app/public ./public
CMD node ./app.js
これで本番環境のビルドは終わりです。
Caveats
Docker で動かす場合は rebuild が必要
注意点として、Docker で動かす場合は npm rebuild esbuild
をコンテナ側で走らせる必要があります。これは、Mac OSでインストールしたバイナリが、コンテナ側 (Alpine Linux) で動かないため、rebuild し直す必要があるためです。
CI等で型チェックが必要
esbuild は型チェックがされないので、CI等どこかのタイミングで型チェックをしてあげる必要があります。そこでは tsc を走らせて上げる必要があります。
結果どうなったか
tsc を使った場合、Transpile は10数秒かかっていました。esbuild の場合、約1秒で Transpile することができ、10倍ほどビルド時間を削減できました。これは、現状ほとんど TypeScript のファイルがないのでこの時間ですが、esbuild のアルゴリズムを見る限り、今後TS化が進んだとしても少ない計算量でビルドできる可能性が高く、時間経過につれて得をします。
微妙かもしれない点
プロジェクトによっては微妙かもしれない点を書きます。
ビルドの登場人物が増えて複雑になる
esbuild がビルド周りの登場人物に加わります。
例えば、自分が導入したプロジェクトでは、Node.js+Expressをカスタムサーバーとして Next.js が動いていました。Next.js のビルドが入らないカスタムサーバー部分をTS化する場合、tsc を使う必要があるのは仕方ないとして、その上でさらに esbuild だと・・・?のように思う人はいるんじゃないかなと思います。
今回に関しては、esbuild の責務が Bundler というよりは Transpiler だということ、いざとなれば置き換えも容易にできることを考え、問題にならないと判断しました。
ラッパーが2つ
今回、esbuild をそのまま使うのではなく、開発環境と本番でそれぞれラッパーを使用しました。こちらは、前述の登場人物増える問題を加速させていると捉えることもできそうです、
こちらも、ラッパー自体は置き換えが容易であること、ラッパーのソースコードを読み込んだ上で、自分で同じようなスクリプトを書くのも難しくないことなどから、(なるべくデフォルトの設定を使う前提で)採用しました。
Bundler ほどは差が出ない
フロントエンドの Bundler であれば、1分以上ビルドにかかることもよくあります。その場合、esbuild 化したときの差分が何十倍にもなるので、導入の効果が大きいと言えます。
今回は、バックエンドの Transpiler として使用しているため、フロントエンドに使うほどの差はでにくいところはあると思います。
rebuild が必要
前述した、コンテナ環境で rebuild しないといけないのが地味に嫌です
まとめ
考慮するポイントはありますが、リリース後も安定して稼働しており、esbuild を導入したメリットは大きかったと考えます。
Node.js をバックエンドに使っている方は試してみてはいかがでしょうか。
Discussion