こんにちは。みなさまがCacooで図の編集をするエディターを開発している、Cacooチームの国広です。Cacooチームでは、エディター部分のビルドに Webpack を使用しています。ビルド対象はTypeScript、JavaScript、PostCSS、Riot(のtagファイル)、画像等です。
そのままビルドするには少し我慢ならない程度に待たされるようになったので、Webpackの公式ドキュメントを見ながら少しだけビルドのパフォーマンスの改善を行いました。その結果、ビルドにかかる時間は当初のシンプルな構成に比べて6倍早くなりました。
対象読者
- Webpackを使用している人
- ビルドのパフォーマンスを改善したい人
Webpackのビルドパフォーマンス改善
ビルドとは何か
現在のCacooのエディターでは、いくつかのJavaScriptファイルをHTMLに読み込ませることでアプリを起動させています。CSS、画像などもその中に含まれています。
しかし開発段階では開発者全員が一つのファイルで開発するというのは現実的ではなく、またTypeScriptやPostCSSなどのトランスパイルが必要な言語を使用しているのもありどうしても最終成果物を吐き出すビルドという処理が必要になってきます。
そこで私たちが使用しているのがWebpackです。図のようにWebpackは様々な依存関係があるファイルを特定のファイルにまとめてくれます。
今回は開発の効率を上げたいだけなので、最終的な成果物のビルドの話は置いておきます。
私たちはファイルを変更して、ビルドが終わったらブラウザ上でその動きを確認しています。つまり、ファイルの変更から最終的な動きを確認できるまでの時間を短縮すれば生産性の向上につながります。
ですので、ここではwebpack-dev-serverの立ち上げと、ファイルを監視して変更を検知し、ブラウザがリロードし始めるまでとします。(ファイルの変更も一行変更させるだけなのであまり意味のある指標は出ないですが、体感するためとして数値を記録していきます。)
計測する
今回の施策を施すまでの時間を計測してみます。
以下web-dev-serverのプロセスを立ち上げることを初回ビルド、web-dev-serverのインスタンスが立ち上がった状態でのホットリローディングによるビルドは差分ビルドとして話を進めます。
- 初回ビルド
約60秒 - 差分ビルド
約10秒
特に初回ビルドは1分近く待たされて、大変苦痛でした。
対策
ここからはWebpackのビルドパフォーマンスについての章見てから行った施策を並べていきます。
Nodeのバージョンをあげる
長らくNodeのバージョンはv6を使用していたのですが、きちんと最新にあげることにしました。
現在Cacooチームでは使用するNodeのバージョンをnodenvを使用していますので、開発者全員の使用するNodeのバージョンを揃えることが簡単にできます。
- 初回ビルド
約46秒 - 差分ビルド
約7.5秒
TypeScriptのビルドを並列処理にする
まずTypeScriptのビルドを並列に動かせるようにします。
TypeScriptのビルドには2つの処理が存在します。
- 型のチェック
- TypeScriptからJavaScriptへの変換
ts-loaderのREADMEをみると型のチェックを行わずにtranspileOnly: true
としてTypeScriptからJavaScriptへの変換のみを行うのがビルドを速く行う手段だと記載されています。
では型のチェックはどうするかというとFork TS Checker Webpack Pluginを使用します。このプラグインのおかげでトランスパイルと型のチェックを別プロセスとして処理することができます。
さらにts-loaderの処理自体を並列にしてみましょう。thread-loaderを使用することでts-loaderの処理自体を並列化させます。thread-loader自体は様々なloaderに使用できるので必ずしもts-loaderのみに適応する必要はないです。
Nodeの処理系についてあまり理解できていないですが、thread-loaderのREADMEによると新たなWorkerプロセスを実行するのに~600msの時間を要するとのことなので、使用どころは試しながら、最適解を探すのが良いかもしれません。
今回他のloaderにも適用してみましたがパフォーマンスの向上が見られなかったので、TypeScript部分にのみ使用しました。
最終的にTypeScriptのloaderの設定はこのようになります。
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
},
{
loader: 'ts-loader',
options: {
transpileOnly: true,
happyPackMode: true
}
}
]
}
説明してなかったWorkerの数についてですが、Github上のサンプルをみるとts-fork-checkerのために1つ残しておくのが良いようです。
ts-loaderのみで簡単に設定できていた頃と比べると少し複雑ですが、これだけの設定でビルドの待ち時間が短縮できるのでやる価値はあります。
- 初回ビルド
約24秒 - 差分ビルド
約6秒
依存するライブラリを減らす
公式ドキュメントでもSmaller = Faster
と言及されている通り依存ライブラリの削減も効果的です。
現在のCacooのエディターはjQueryやそれにひもづくUIライブラリに依存しています。現在開発中の次期エディターでは不必要(ライブラリに依存しなくても作成可能もしくはそんなに手間のかからない)な依存ライブラリを削除するように進めています。
まずは依存ライブラリを視覚化したいのでWebpack Bundle Analyzerを使用して依存ライブラリを明らかにします。
現在のCacooエディターで使われてるライブラリ
開発中の次期Cacooエディターで使われてるライブラリ
依存ライブラリを 1.4MB => 600KBまで削減することができました。(SockJSはCacooのエディターでは使っていませんが、webpack-dev-serverの依存です)
次期Cacooエディターでは概ね既存のソースコードを流用していますが、まだ機能が足りていないこともあり、厳密な比較はできませんが、結果に改善が見られました。
- 初回ビルド
約20秒 - 差分ビルド
約5.5秒
ディスクキャッシュを使用する
以下のプラグインを利用することで初回ビルドの苦痛を軽減することができます。
Cache Loader
このloaderは各ファイルの依存関係やトランスパイル後の結果をファイルにしておき、次のビルドからはその結果を使うことで該当loaderの処理をスキップします。
READMEによるとキャッシュファイルのReadとWriteに時間がかかる可能性もあり、重めのloaderのみ使用するのが良いとのことです。実際に全てのloaderに付け加えるとかえってビルドが遅くなったので間違いなさそうです。
PostCSS、TypeScript、画像に使用していました。
Hard Source Webpack Plugin
Cache Loaderと同じくディスクキャッシュを行うプラグインです。Cache Loaderが各ファイルの結果をjsonファイルとしてキャッシュするのに対して、ローカルに作成したLevelDBにキャッシュを配置します。体感で最初のビルドはCache Loaderより重くなるのですが、2回目以降のビルドは早くなります。
Cache LoaderとHard Source Webpack Pluginは共存もできますが、ディスクキャッシュするプラグインを2つ置いてキャッシュファイル数が増えたり、混乱が起きたりすることを避けるために、共存させたくありませんでした。
前述の通り一回ディスクキャッシュを作ってしまえば、そのあとはHard Source Webpack Pluginが早かったため、後者のみを使用することにしました。
最終結果
- 初回ビルド
約10秒 - 差分ビルド
約5秒
結果として、初回ビルドは当初のシンプルな構成に比べて6倍早くなり、差分ビルドも2倍程度早くなりました。
色々な施策を講じましたが、個人的にはHard Source Webpackと並列処理による効果が大きいように感じられました。もちろんNodeの処理系の速度が上がっていることも見逃せません。
※Hard Source Webpackの効果を入れたいので一回キャッシュを作った後の数値です
※編集するファイル(ts, css, js etc)やその時のローカルの状況などによって、この数値は大きく変化しますが、どのような場合にも概ねパフォーマンスが向上していることが体感できました。特に差分ビルドはWebpackがWatchモードでメモリ上にキャッシュをデフォルトで作成するため、何回か同じような試行を行うと、勝手にパフォーマンスが改善されるため計測しにくいです。(参考資料:https://webpack.js.org/configuration/other-options/#cache)
反省点
どのloaderが特に重く、どんな施策を行えば効率的になるかのメトリクスを取ることができず、結局場当たり的な対応となってしまいました。この部分の反省を改善できるように勉強していきたいです。
また設定が複雑になってしまったこともあり、他のモジュールバンドラーの検証もしていきたいところです。
まとめ
- 何事も最新にしておくの重要
- 色々面倒ですが、ビルドのパフォーマンスがあげられるのでいろんなプラグインを試してみるのオススメ
- Webpackのドキュメントとプラグインの充実がすごい
- Parcelも気になる
Webpackに限らず、ビルドスクリプトのメンテナンスは後回しになりがちですが、たまには見直して少しずつ改善していくのも楽しく感じられました。
NulabではCacooの次期エディターを一緒に開発したいエンジニアを募集していますので、興味がある方はぜひお問い合わせください。