こんにちは。株式会社SocialDog人事のまなてぃです。
こちらは、過去にリリースしたTech Blogです!
SocialDogの志潟寛生です。
この記事では、2021年10月にwebpackからesbuildに移行することで、SocialDogのCIビルドのうち、JavaScript部分を264倍早くした事例についてご紹介します。
モダンなビルドツールであるesbuildに移行した際に発生したトラブルとその対処についてご紹介できればと思います。
ビルドが遅い問題
esbuildに移行する以前は、Cypressで使用するコードはwebpackでビルドしていました。
Cypressを並列化するなどして何とか高速化していたものの、webpackのビルド時間には9分50秒ほど掛かっていました。webpackのビルドが終わらなければ後続の依存したタスクが実行できないため、CIの実行時間の大きなボトルネックとなっていました。
手元の開発環境で本番ビルドでCypressを回すときも毎回10分前後待たねばならず、私の主観ではCypressのリグレッションテストを書くモチベーションがかなり低下していました。Cypressが落ちた場合も、修正して10分待たないと確認できないのが不便でした。
そこで、出力結果を変えずに、できるだけ早くビルドする方法を探ることになります。
期待の新星esbuild
esbuildはGo [1] で実装されているJavaScriptのビルドツールです。
webpackよりも後発のビルドツールで、よりパフォーマンスと容易さにフォーカスしています。公式で10~100倍早いビルドツールと銘打っています。
その早さは折り紙付きで、CyberAgentの開発環境を69倍高速化する のに使われた実績がありました。
公式ウェブサイトによれば、ES6とCommonJSモジュールに対応し、ES6モジュールではTree Shakingまでやってくれます。
TypeScriptやJSXにもデフォルトで対応しています。
導入にあたるトラブル
とはいえ、すんなり導入できたわけではありません。esbuildもwebpackと完全に互換性があるわけではないため、いくつかのトラブルが発生しました。
CSS importが動かない
従来のwebpack環境ではcss-loaderやsass-loaderを使用していました。SocialDogの依存ライブラリの1つであるTwemojiではCSSを直接importしている箇所があり、esbuildに移行するとそのままでは動作しませんでした。
対応方法として、該当箇所のCSSはGulp側のビルドに含めるようにしました。
AMD Moduleが動かない
Asynchronous Module というモジュールシステムがあります。
Requireというグローバルオブジェクトの下にモジュールを生やしていくという仕組みです。
esbuild移行前には、ドラッグ・アンド・ドロップでjquery-uiを使用しているコードが残っていました。しかし、esbuildではAMDはサポートしておらず、jquery-uiをRequire以下に読み込むことができず、実行時エラーが発生してしまっていました。
AMDをサポートしてほしいというIssue も立っていましたが、実装当初から記事執筆時の現在でも2021年11月を最後に動きがなく、今すぐ対応される予定はなさそうです。
直接importしてみたり、別のscriptとして読み込んだりといろいろ試してみましたが、どれもうまくいきませんでした。esbuildでビルドした時点でRequireJSに依存関係を作るようにビルドする方法をなにかご存知でしたら、ぜひ教えてください。
幸いにも、jquery-uiを使用している機能が廃止予定だったため、その機能が廃止されるまで待つことで対応しました。
導入手順
esbuildを実行するスクリプトを書きました。
esbuildは直接CLIから動かすこともできますが、Nodeから実行することもできます。今回は、実行前後にSCSSファイルの変換などのPre/Postスクリプトを挟みたかったことや、引数によってWatchの切り替えをしたかったので、実行用のスクリプトファイルを書いて、それをnpm scriptから実行するようにしました。
以下にスクリプトの一部を掲載します(実際に使用しているものには型チェックを行うためのコードなども含まれており、多少内容が異なります)
const esbuild = require('esbuild');
const { spawn } = require('child_process');
const args = process.argv.slice(2);
const OUT_PATH = 'build/bundle.js';
const ENTRYPOINT_PATH = 'src/index.tsx';
esbuild.build({
bundle: true,
watch: args.includes('--watch') && {
onRebuild: (err, res) => {
const now = `[${new Date().toLocaleTimeString()}]`;
if (err) {
console.error(`${now} ${err}`);
return;
}
console.log(`Rebuild done!`);
if (res.warnings.length) {
console.log(`${now} ${res.warnings}`);
}
},
},
entryPoints: [ENTRYPOINT_PATH],
outfile: OUT_PATH,
sourcemap: true,
platform: 'browser',
target: 'es6',
tsconfig: 'tsconfig.json',
define: {
'process.env': '{}',
'process.env.NODE_ENV': args.includes('--development')
? '"development"'
: '"production"',
global: 'window',
},
minifyIdentifiers: !args.includes('--development'),
minifyWhitespace: true,
minifySyntax: true,
});
どれぐらい早くなったのか
ビルド時間を 約8分半 短縮することに成功しました。
ビルド待ち時間が約25分から約16分まで短縮されました。
これにより、CIが落ちて修正が必要なことに気づくまでの時間が8分半も短縮されます。
金銭的メリット
GitHub Actionsには1分あたり0.008ドルが掛かるので、ビルド1回につき0.068ドル節約したことになります。
少ない日でも1日あたりおよそ10回程度実行されているので、毎日100円ぐらいは最低でも節約できているでしょうか。
キャンセルを含めますが、esbuild導入から2022/03/01までに約2,914回実行されたので、約198ドル、2万円程度は節約できたことになります。
開発環境が軽くなった
開発環境でwebpackとesbuildのどちらを使用するか選べるようにした結果、なんと社内のほとんどの開発者にesbuildを使ってもらえました。
webpackも初回ビルドが遅いのが気になっており、package.jsonの更新やブランチ切り替えの度に10分弱も待たされることがなくなりました。
インクリメンタルビルドもwebpackでは十数秒ぐらいかかっていたのが、esbuildでは一瞬になりました。
esbuildにはHMRがないというデメリットもありますが、それ以上にビルドが早いというメリットが社内では好まれたようです。
結果的に、現在では社内の開発環境ではesbuildがデフォルトで使われるような設定になっています。
デメリットとリスク
本番環境は引き続きwebpackでビルドしているため、若干UIに差が出てしまう箇所がまれにあるようです。特に面倒なのが、webpackビルドとesbuildビルドでCypressの画像差分が出てしまうケースがあるようです。
esbuildは後発な分エコシステムが未成熟で、プラグインが十分に出揃っていないため、ある程度自力でどうにかすることを求められます。
ほかにも、esbuildは開発途中で、webpackで使っていた機能やプラグインが使えないというデメリットがあります。
esbuildに移行する際に使えなくなってしまった機能の1つとして、webpackには画面をリロードせずともソースコードの変更点を更新してくれるHMRがありますが、この機能はesbuildにはありません。
また、babel-plugin-styled-componentsなどの数多くのプラグインが使えなくなります。
型はesbuildでは読み飛ばされてチェックされないため、TypeScriptの型は tsc --watch --noEmit など、他のツールでチェックする必要があります。
導入当初はesbuildはまだベータ版に近い状態で、十分に安定はしているものの本番環境に使うにはまだ不安があったため、masterブランチや本番環境はwebpackでビルドしています。SocialDogでは本番ビルドまでをesbuildに置き換えてはいないので、複数のビルド環境を同時に維持していくコストが掛かります。
しかし、これらのデメリットとリスクを許容したとしてもビルドが8分から数秒になる点は魅力的であり、開発環境とCypress環境ではesbuildを使用しています。
まとめ
esbuildのスピードはとても魅力的です。ビルド速度が上がるだけで大きく開発体験が改善するため、ぜひ触ってみてはいかがでしょうか?
SocialDogではesbuildのようにスピーディに開発できるメンバーを募集しております!
ぜひ興味があればご応募ください。
リンク
- esbuild - An extremely fast JavaScript bundler
- evanw/esbuild - GitHub
- TypeScript: Documentation - tsc CLI Options
- RequireJS使い方メモ - Qiita
注釈
[1]: 元々はGoとRustで実装比較されましたが、 Goの方がコンパイルが早い、TypeScriptのトランスパイルのコードを移植する際にGCがあったほうがそのまま移植しやすいという経緯がありGoに移行しました。Rustで実装されている swc とは対照的です