📘

esbuild で Node.js を TypeScript 化する

4 min read

TLDR

  • バックエンドのコードを esbuild で TypeScript 化した
  • ビルド時間は1秒ほど
  • tsc に比べて10倍以上高速化できた
  • 特に問題なくプロダクションで安定稼働している

esbuild はいつ使えるのか

みなさん webpack は使っていますか?まだまだ広く使われていると思いますが、最近では esbuild の事例もたまに見るようになってきました。

現時点で 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 --from=builder /home/node/app/dist .
COPY --from=builder /home/node/app/node_modules ./node_modules
COPY --from=builder /home/node/app/public ./public

CMD node ./app.js

これで本番環境のビルドは終わりです。

Caveats

Docker で動かす場合は rebuild が必要

注意点として、Docker で動かす場合は npm rebuild esbuild をコンテナ側で走らせる必要があります。これは、Mac OSでインストールしたバイナリが、コンテナ側 (Alpine Linux) で動かないため、rebuild し直す必要があるためです。

https://github.com/evanw/esbuild/issues/1223

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 をバックエンドに使っている方は試してみてはいかがでしょうか。