話題のesbuildをさっくりと調査してみた

TopPage

こんにちは! 勤務先での担務表に一言「イノベーション」と書かれているhedrallです。

今回は今大注目のesbuildに関してさっくり(自信なさめ)と調べてみました。

きっかけ

私がフロントを本格的に勉強し始めたのは2016-2017年のAngular2が出たころです。モダンフロントエンド開発の選択肢が増え、 PWAが提唱されるなどWEBフロントエンドが一気にリッチ化すると同時にカオス化した時期だったと思います。 つまり、本当に楽しい状況だったのですが最近になってその状況も落ち着いてきたと感じている方も少なくないと思います。

昔よくあった、React vs Angular vs Vue論争の熱は冷めたし、 SSRだけでペラペラだった(ドキュメントもダサかったw)Next.jsも2019~2020年の大規模機能拡張でフルスタックフレームワークとして堂々たるツールに成長し、今では画像配信サーバを内臓し、画像の最適化まで面倒をみてくれます。 この背景として、先立って機能拡充が進んでいたNuxt.jsからのいいとこ取りができたところも大きいと思います。

私はAnguler, Nuxt, Nextを業務で使用したことがありますが、どのライブラリやフレームワークを使っても大体のことはできるし、 お互いを比較した時に、突出した優位性などはほとんどないと思っています。(個人的にreactが書いていて一番しっくりくるという好き嫌いくらいの話です。)

この成熟したフロントエンド環境の最後の大課題といっていいのがコンパイラとバンドラのパフォーマンスといってもいいと思います! 2020年は上述の背景の中、SPA系のプロジェクトの注目度も依然高い状況ですが、新たにビルドツール系のstar数の急激な伸びが印象的です。

ビルドツールのトレンドランキング!こちらをご参考

| 順位 | 名前 | 増加数 | | :-- | :-: | :-: | | 1 | esbuild | +16.6k | | 2 | Rome | +14.2k | | 3 | Vita | +14.1k | | 4 | Snowpack | +10.1k | | 5 | Webpack | +4.5k |

う〜ん、とはいえみなさん、ビルドツールってなんか地味な印象ありませんか?私はそうでした。 ビルドツールといえば、フロントオタクがwebpackをゴリゴリチューニングしたり、rollupなどの色々ツールがあるもいまいち根本的にビジネスメリットがあるわけでもないし、 下手に手を出すと技に溺れやすい世界だし、第一最適化はフレームワークに任せればいいじゃん!っと考えていました。

しかし、今回注目したいesbuildはまさにフロントエンドの次の時代を作ってくれる様な、キラキラとした期待を抱くことができます。

esbuildとは

webpackなどに変わるフロントエンドのコード変換+バンドルツールで、なんとWebpackと比較して10-100倍速度が早いそうです。 これが本当だとすると、フロントの開発効率は根本的に変わってくると思います。

当然の話ですが、現在のWebpackを使った各フレームワークのビルドはめちゃめちゃ遅いです。 nextやnuxtなどフレームワークで初期プロジェクトを構築し、デザインフレームワークを一つでも挿そうものなら、既に数十秒のビルド時間を覚悟する必要があります。 開発効率の向上のためのdev serverは、バンドルサイズが非常に重くブラウザのパフォーマンスが非常に厳しいものがあります。
また、HMRも実装が複雑らしくうまく反映されないことがあったり、SSRと相性が悪かったりと有効なシチュエーションが限られている現状です。 特に本番系でしか出ないエラーに遭遇した際は本当に泣くしかありませんw 数分(數十分)かけてビルドして、修正して、もう一回ビルドという経験をされている方も多いと思います。

この様に中規模以上のフロントエンド開発経験者であれば、「ビルドの遅さ」との戦いは開発プロセスの根本的な課題であることが実感としてあると思います。 よって、この部分が解決されるというのは正にエポックメイキングで、夢の様な話だなぁと感じます。 (本当かなぁ?)

esbuildの調査に当たってはアーキテクチャーなども見てみましたが、 the-super-tiny-compilerを読んだ程度では全く知識が足りずさっぱりでした、、w そのせいか、開発は殆どEvan一人で進めている様です。

ちなみに、esbuildは2021/01時点でスター数が19kなので、彗星のごとく現れたということが分かります。 さらにランキング3vitaはesbuildに依存しています。vueはビルドプロセスが独特なので、esbuildとは別のレイヤーで対応するべきとVueのEvanとesbuildのEvanが git上で議論しています。
(kabukuさんの記事 より。)
4位のSnowpcakもesbuildに依存しています。

試してみた

create-next-appでプロジェクトを作成し、起動方法をreact-scriptからesbuildに変更してみました。 具体的にはプロジェクトのルートに下記のスクリプトを作成し、node build.jsを実行することでビルドします。

// build.js
const watch = process.env.WATCH === 'true';
console.log( process.env.WATCH, watch );
require('esbuild').build({
  entryPoints: ['src/index.tsx'],
  bundle: true,
  outfile: 'dist/out.js',
  define: {
    "process.env.NODE_ENV": '"development"'
  },
  loader: {
    '.svg': 'dataurl'
  },
  minify: false,
  sourcemap: true,
  target: ['chrome70', 'firefox57', 'safari11', 'edge16'],
  watch,
}).catch(() => process.exit(1))

webpackを使用したことがある方であれば、なかなかシンプルで分かりやすい設定だと思います。 これだけの設定でバンドルを生成してくれます。

実際に実行してみました。

time node build.js
> real	0m0.282s

これはっっ、本当に早い! 初期状態のプロジェクトでもdev-serverを立ち上げるまでに数秒は待たされる所なので、春の日差しの様な清々しさを感じます。

esbuildできないこと

esbuildはまだ実験段階のプロダクトであるということと、パフォーマンスのため出来るだけシンプルな機能群に絞っているということもあり、出来ないこともはっきりしています。

私が確認した限りですと、下記はesbuildで実行することが出来ません。

  • es5への変換
  • CSS Modules
  • Code Splitting と import() による lazy loading
  • next.jsでの利用 ( next-babel-loaderの置き換えが難しい )
    • 議論 => https://github.com/vercel/next.js/discussions/16152
  • ポストCSSの利用
  • HMR

AMEBAさんの記事もご参考

HMRに関しては対応の優先順は相当低そうです。(議論 => https://github.com/evanw/esbuild/issues/97)
こちらの議論によると、HMRはそもそもビルドが遅く、検証バンドルが重いWebpack環境ではデザインの編集など一部のユースケースで有効なこともありますが、 esbuildはそもそもビルドが爆速なので需要がそこまで高くないのと、HMRは複雑で実装コストが非常に高いということがあげられる様です。

esbuildができること

差分ビルド

watch: trueをすることで差分ビルドを走らせることができます。無論超高速です。

ベースパスの変更、パスエイリアスの使用

TypeScriptを使用しているとベースパスの変更、パスエイリアスを使いたいことがあります。

// tsconfig.json
{
  "baseUrl": "./src/",
  "paths": {
    "@/*": ["./*"]
  },
}

// index.tsx
import App from 'components/App';
// or
import App from '@/components/App';

これは、自動的にパスを解釈してくれる様です。webpackだと、resolve.aliasを調整しないといけなかったりするので便利です。

CSS import

よくプロジェクトのルートでcssをimportしたいことがあります。

// index.tsx
import App from 'assets/styles/global.css';

この場合、index.tsxをバンドルすると、自動的にimportしているcssを全てまとめて、一つのcssファイルとして書き出してくれます。 今回のビルド設定だと、dist/out.cssが生成されます。

デザインライブラリの利用

material-uiと、私が最近贔屓にしているantdを読み込んでみましたが普通に使えました。 antdはcssを別途読み込む必要があり、公式ページのgetting startedではcssファイル内にimportを記述する方法が記載されていますが、 うまく行かないので、index.tsxでimportしてあげる様にすると動きました。

本当に早いのか

antdのボタンを一つ配置した状態でビルドをおこなってみました。

import { Button } from 'antd';

まずはWebpack選手。下記の通りwebpcak.configを書いて実行しています。 計測には time と speed-measure-webpack-pluginを利用しました。

const path = require( 'path' );
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  mode: 'development',
  entry: path.resolve( __dirname, 'src/index.tsx' ),
  output: {
    path: path.resolve( __dirname, 'dist' ),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.(jsx|tsx|ts|js)$/,
        exclude: /node_modules/,
        loader: 'ts-loader'
      },
      {
        test: /\.(svg)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },
      {
        test: /\.css$/,
        loaders: ['style-loader', 'css-loader'],
      },
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx', '.tsx', '.ts'],
    alias: {
      '@': path.resolve( __dirname, './src' ),
    },
    modules: [
      path.resolve( __dirname, './src' ),
      path.resolve( __dirname, './node_modules' ),
    ]
  },
  plugins: [],
});

実行

time npx webpack --config webpack.config.js

結果

 SMP  ⏱
General output time took 8.73 secs

 SMP  ⏱  Loaders
ts-loader took 5.24 secs
  module count = 4
modules with no loaders took 2.99 secs
  module count = 1030
css-loader took 0.449 secs
  module count = 3
url-loader took 0.066 secs
  module count = 1
style-loader, and
css-loader took 0.037 secs
  module count = 3

real	0m10.665s

10秒ちょっとなので、そんなもんかなぁという感じですね。

続いてesbuildです!

実行!

time node build.js

結果

real	0m0.608s

再び、春の日差しの様な清々しさと額の辺りに微かな清涼感を感じました。 esbuildの方が約18倍高速です

さらに esbuild-loaderなるものがあって、 webpcakのbabel-laoderやts-loaderを置き換える様にして使用することできます。

esbuild-loaderではcss importの解決ができない様で、ビルドを通すことができなかったのですが途中までのログはこんな感じです。

SMP  ⏱
General output time took 4.055 secs

 SMP  ⏱  Plugins
ESBuildPlugin took 0.01 secs

 SMP  ⏱  Loaders
esbuild-loader took 3.67 secs
  module count = 1033
modules with no loaders took 0.019 secs
  module count = 3

むむ?esbuild-loaderの所で3.6秒も使ってますね。実際にesbuild-loaderのissueをみても、 倍程度にしか高速化していないので、こんなものなのかもしれません。(それでも最高ではありますが。)

まとめるとこんな結果になりました。

| ビルド方法 | 時間(約) | ファイルサイズ | | :-- | :-: | :-: | | Webpack(dev) | 10.6秒 | 6.92Mb | | Webpcak(prd) | 18.2秒 | 2.16Mb | | esbuild(dev) | 0.6秒 | 1.1Mb + 710kb | | esbuild(prd) | 0.6秒 | 420kb + 720kb |

esbuildの場合、jsとcssは別バンドルになるので、ファイルサイズはjs, cssの順で記載しています。 これを見ると、本番ビルドではなんと30倍も高速であることが分かります!またファイルサイズもいずれもWebpackより小さくなっている様で、 開発時のブラウザの負荷も段違いだと思います。

実践投入に関して

まだesbuildは実験段階に位置付けられていますが、vita, snowpackなどすでに多くの新興ツールが依存している状態になり、今後の発展は非常に期待ができると思われます。
しかしながら、やっぱり本番運用まだ怖い場合は、ABEMAさんの記事でも提案されている様に、 検証系のビルド方法に一手間加えて、DXの爆上げという目的が使いやすいと思います。

将来的にエビデンスなどが揃いstableなツールとなれば、超高速CI/CDの実現など夢は広がります。

周辺ツールに関して(2021/9/26追記)

esbuildそのものを実戦投入しなくても、部分的にesbuildの力を活用できるツールがあるのでご紹介します。

build-register

TSファイルを直接実行したい時、従来であれば ts-node を利用することが多かったと思いますが、実行の度に変換に時間がかかっていました。 esbuild-registerを利用すると、従来より高速にTSファイルを実行することができます。

node -r esbuild-register hoge.ts

実際結構早いので、ぜひお試しください。

esbuild-jest

JestでTypeScriptのspecを実行する場合はtransformに ts-jest を指定するのが一般的かと思いますが、こちらも実行の度に変換に時間がかかります。 そこで、 ts-jest をそのまま esbuild-jest に置き換えることで、esbuildによるtransformに置き換えることができます。

module.exports = {
  // ...
  transform: {
    '^.+\\.(ts|tsx)$': 'esbuild-jest'
  }
}

私の試した範囲では、感覚的に2/3くらいの実行速度に改善しました。また、React周りのテストでエラーが発生したので、バックエンドコードのテストに利用しています。

まとめ

esbuildはやっぱり早かった!SPAページを作成する際はあえて、nextを断念してでも導入を検討したいと思いました。

また、DenoやRomeなどのエコシステムのネクストジェネレーション?的なツールがどんどん出てきていますので、 近いうちに調査、比較を行ってみたいと思います。

長文になりましたが、最後まで読んでくださった方は、ありがとうございました。

ご意見ご感想などはtwitterまで頂けると嬉しいです。