パーソンリンクアドベントカレンダー 1日目の投稿です。おはようございます。
今私が担当しているプロジェクトでは、フルDockerの開発環境の上で、RailsとVue.js(+typescript)を使ったWEBサービスを開発しています。その関係もあり、WebpackをRailsで管理できるWebPackerを用いて開発を進めていました。
私が会社に入社して最初のプロジェクトにJoinしたのがほんの2ヶ月前。Join当時はフロントのビルドにめちゃくちゃ時間がかかっており、大層イライラしてました。ビルドが終わるまで缶コーヒー一本開けられるくらいの時間待たされおり。SIer時代のPC起動待ちを思い出してちょっとノスタルジックなお気持ちに(´・ω・`)
それもそれでヤバいんですが、極めつけはWebpackのDevServerが動いていないこと。これじゃ修正するたびに長くビルドを待つ羽目になってしまう...
DevServer動くは動くんですが、ビルドにメモリを使いすぎていてDevServerが新しいビルドを動かそうとすると、メモリ使いすぎてプロセスが殺される始末。
一応、NODE=OPTIONのmax-old-space-size
=4Gと、Dockerコンテナのメモリを増やして対処していたようですが、それでもまれによくプロセスが殺されてしまい安定せず...。結局問題が解消するまで、コードを修正するたびに都度yarn build
を叩き、皆コーヒー一本分の時間を耐え忍んでいたのでした。
本日はその困ったビルドを爆速にした話を書きます(`・ω・´)
本文に入る前の会社紹介など
さて本文に入る前に、アドベントカレンダー初日なので会社の紹介をします。
株式会社パーソンリンクは、WEBサービス開発のクライアントワークを主軸にしている9期目の開発会社です。広告やビッグデータのシステム構築・運用に力をいれていて、主要な取引先も広告系の会社が多いです。
平均年齢が20台半ばという業界でも若めな雰囲気の会社で、エンジニア未経験の人も多く入社されてます。今でも積極採用中なのでもし興味がある方いましたら、ぜひお気軽に見学にいらしてください(`・ω・´)
今回のアドベントカレンダー、会社で初参加ということもあり、いろいろと至らない点があるかも知れませんが、読んで頂けると嬉しいです。
それでは本文へ。
本文
本記事では以下の環境を前提として説明しています。
MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports) Mojave
CPU: 2.3 GHz Intel Core i5
RAM: 16 GB 2133 MHz LPDDR3
rails (5.2.3)
webpacker (4.0.7, webpack 4.37.0)
vue (2.6.10)
yarn (1.13.0)
typescript (3.5.3)
遅かった当時のコードでビルドを動かした結果、かかった時間はこんな感じです。
bin/webpack
> Hash: 825161974e795ca3cc4b
> Version: webpack 4.37.0
> Time: 132537ms
132秒。
当時感じていた体感より短かった( ゚д゚)缶コーヒーグビッとキメるくらいの時間でしたね。
しかしWebpackerでのビルド中の最大使用メモリは3.2GB!!!これが修正するたびにかかると思うとかなりしんどい.....
# docker statsの結果。ヤバい。
18f0ad218beb app_1 147.65% 3.204GiB / 4.834GiB 66.29% 1.67kB / 1.01kB 120MB / 0B 24
さて、ビルドの高速化ですが、Webpackerでも基本的にWebpackと同じビルド高速化の手法を活用できます。色々と試したんですが、結論、著しく効果が見られたものはこの4つでした。
- Railsの起動時に実行していたWebpackerビルドを無効化する
- HardSourceWebpackPluginを使う
- sourcemapの出力をやめる
- splitChunksを有効化する
それぞれ解説していきます。
Railsの起動時に実行していたWebpackerビルドを無効化する
こちらはフロントのビルド自体が遅い話と関連は薄いのですが、バックエンドの立ち上がりの遅さがチーム内の課題に乗っていたので真っ先に対応したやつです。
Webpackerを使っている場合、Webpacker.ymlにcompileというオプションがあります。
https://github.com/rails/webpacker/blob/master/lib/install/config/webpacker.yml#L53
これは、Railsが起動した後、APIにリクエストを受けたときに自動でビルドを走らせるオプションでした。
時間がめちゃくちゃかかるビルドがRailsを立ち上げてリクエストを受けたときに走る。バックエンド側のエンジニアも起動時にかなり待つというありさま...さすがに全体の生産性ガタ落ちなので、まずはこれを着手しました。
とはいえ、この対応はすごく簡単で、compile
オプションをfalseにしてあげるだけです。
development:
<<: *default
compile: false
これだけで、バックエンドがいちいちフロントのビルドを待つ必要がなくなり、素早くAPIサーバーが立ち上がるようになりました。
しかし、その影響もありフロント側の動作確認をするときは任意で起動しないといけなくなってしまいました(´・ω・`)なるべくめんどくさいのを排除したかったので、こんなMakefileを書いて使ってもらうようにして解決です。
yarn/build:
docker-compose run --rm app yarn build
"scripts": {
"build": "yarn install && NODE_OPTIONS='--max-old-space-size=4096' ./bin/webpack",
"dev": "yarn install && NODE_OPTIONS='--max-old-space-size=4096' ./bin/webpack-dev-server"
}
HardSourceWebpackPluginを使う
Webpackでのビルド高速化で真っ先に思い浮かぶのがこれ!!
Nuxt.js でビルド時間を100倍高速化して作業時間が大幅upした件(HardSourceWebpackPluginの紹介) - Qiita
皆さんも上の記事を見られたことあると思います。
一度ビルドした状態をキャッシュに持っておき、変更がないところはキャッシュに入っているものを使うというプラグイン。初回はフルでビルドしちゃうので変わらないんですが、二回目からキャッシュが効き劇的に変わります。
まあ、ビルド時にメモリを大量に使い果たす現象はこれではそれほど解決できなかったんですけどね....
とはいえかかる時間のわりに効果が大きいため、即効性を優先してこちらを対象した流れになります。
これをWebpakcerに適用する場合、config/webpack/development.jsあたりにこんな設定をいれてください。
const { environment } = require('@rails/webpacker');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
environment.plugins.prepend('HardSourceWebpackPlugin', new HardSourceWebpackPlugin());
あ、もちろんyarn add -D hard-source-webpack-plugin
を忘れないように(`・ω・´)
さて、これを適用したらどう変わったか....当時のコミットに戻って計測してみましょう。
before | after(use cache) | |
---|---|---|
1回目 | 132537ms | 123247ms |
2回目 | 129121ms | 28863ms |
6分の1!!!
まあ結果をキャッシュしているんで当然っちゃ当然ですが。これだけでも大分変わりました。ただし、あくまでキャッシュなので、変更があればあるだけキャッシュ作り直さないといけないので、全体の速度改善は引き続き行う必要がありそうです(´・ω・`)
なお、まれにこのキャッシュが壊れてビルドが途中でコケたりします。大きく変更入っている別ブランチに切り替えると起こりやすい気がします。
その時はキャッシュを消せばいいので、ビルドがコケる問題が発生した場合に備えて、こんなmakeを叩いてもらうようにしました。
yarn/clean-build:
docker-compose run --rm app rm -rf node_modules/.cache/hard-source/
@make yarn/build
一応解決
SourceMapの出力をやめる
上の対応をしているときに.map
ファイルが生成されていることに気づきます。
これも根本原因ではなさそうだなーとは思っていましたが、メモリ使用量問題が多少緩和すれば良いかと思ってやった即効性あるやつ。webpackのdevToolオプションで制御できます。
Devtool | webpack
実施前に、フロントエンジニアメンバーにヒアリングしたのですが、SourceMap開くと開くまでめちゃくちゃ時間がかかるということであまり使っていなかったとのこと。なのでサクッと設定して出力しないようにします。
module.exports = {
devtool: 'none', // <-これ
resolve: {
alias: {
before | after(devtool=false) |
---|---|
131203ms | 115772ms |
ビルド時間わずかに低下。12%ほど改善しました。大きな進歩。それよりも一番効果があったのはメモリ。ピークでの使用量が3.2GBから1.9GBに変わりました。いい感じですねー。
開発中SourceMap見れないのはちょっと不便ですが、その場合は個別で設定を有効にしてもらうように周知して、解決。
splitchunkを有効化する
SourceMapを出力しないことで生成されるファイルが減り見通しが良くなりました。
その改善の結果、これまで見落としていた不穏な影を気づかせることになったのでした。
views/class1-a1099cbec5fb6831917a.js 4.23 MiB
views/class2-a1099cbec5fb6831917a.js 4.23 MiB
views/class3-a1099cbec5fb6831917a.js 4.23 MiB
views/class4-a1099cbec5fb6831917a.js 4.23 MiB
....
MB級のファイルが大量にできていました...ざっと数えた感じ25,6ファイルがこんな感じに。
気になるのは一律同じくらいのファイル容量になっていること。普通にVue.jsで実装しているだけなのに、1ファイル4MBとかありえないファイルサイズです。そりゃこんなの生成していたら、メモリ大量に食うわ....
生成されたファイルを覗いてみると....
/***/ "./front/node_modules/axios/lib/adapters/xhr.js":
/*!******************************************************!*\
!*** ./front/node_modules/axios/lib/adapters/xhr.js ***!
\******************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var utils = __webpack_require__(/*! ./../utils */ "./front/node_modules/axios/lib/utils.js");
var settle = __webpack_require__(/*! ./../core/settle */ "./front/node_modules/axios/lib/core/settle.js");
var buildURL = __webpack_require__(/*! ./../helpers/buildURL */ "./front/node_modules/axios/lib/helpers/buildURL.js");
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
var request = new XMLHttpRequest(); // HTTP basic authentication
おわかり頂けただろうか。。。
これはAxiosのコードです。
https://github.com/axios/axios/blob/6284abfa0693c983e9378b2d074c095262aac7bd/lib/adapters/xhr.js#L11
他の4MBファイルにも全部この内容が含まれており、クラス個別毎に、node_modulesのパッケージがBundlingされていることが原因で発生していたことが発覚しました。
そりゃファイルサイズも膨れるし、ビルドの時間もメモリも膨れるわな(ヽ´ω`)
こういった問題のために、splitchunksなる仕組みがWebpackにはあります。
webpacker/webpack.md at master · rails/webpacker · GitHub
node_modulesのパッケージをvendor.jsに取りまとめて、クラス個別にBundlingしないてくれるようになります。
もともとはCommonsChunkPlugin
とう言うものが使われていたらしいのですが、Webpack4からこちらを使うように変わったとのこと。
Webpackerに適用する場合は、以下の修正が必要になります。
environment.splitChunks() // これを追加
<div id="app"></div>
- <%= javascript_pack_tag 'main' %> # ここを消して
+ <%= javascript_packs_with_chunks_tag 'main' %> # これを追加
</body>
果たしてビルド速度はいかほどになるのか!
before | after(use splitChunks) |
---|---|
115772ms | 98483ms |
SourceMapを出さない版と比較して-13%ほど。当初と比較して大体30%ほど削減できたことになります!!
あの大量の4MBファイルは、無事31.1KBまで小さくなりました。一安心。
HardSourceWebpackPluginキャッシュが効かない場合、ビルド速度単体はそこまで大きく変わりませんでしたが、ビルド時の利用メモリ量が劇的に減りました。この対応をいれるとピークが1GBほどになり、当初の3.2Gと比較すると1/3に激減していることが分かります。
ここまでメモリの利用量が下がると、DevServerが正常に動作するようになります!!
DevServerを立ち上げ、おもむろになにかファイルを変更すると.....
ℹ 「wdm」: Hash: bc6b4e0ff57877d8a3e8
Version: webpack 4.37.0
Time: 5703ms
Built at: 11/30/2019 5:38:13 PM
5秒来たー(゚∀゚)ー!
こうして我々のチームは、爆速でビルドできる環境を手に入れ、待たされることない快適な開発ができるようになったのでした。
おわりに
Webpackerのビルド高速化はいかがだったでしょうか?
設定変えるだけのかなりちょっとした対応でしかないのですが、劇的なビルド速度改善効果を得ることができました。
本当ならwebpack-bundle-analyzerも使ってバンドルサイズの圧縮も実施したいところなのですが、今の所、ビルド後のjsのサイズがgzipされて1MBくらいなのと、開発しているサービスがPC向けであるため優先度はそれほど高くありません。モバイル対応の話が出てきた時に、ついでにやっておくくらいの気持ちでいます。
ビルドは開発の要。これが早いか遅いかで大きく開発速度に影響を及ぼします。爆速で開発して早く仕事を終えるためにも大切なことなので、皆様妥協なく改善頑張っていきましょう(`・ω・´)
なお、Webpackerでの高速化をやってきましたが、まもなくこのプロジェクトのWebpackerは、vue-cliへと移行されてしまいます(まさに今移行作業中)
入社して2ヶ月、それなりに付き合って来てようやくWebpackerと仲良くなれたんですが、今後PWAなどの対応が入ってくる予定でvue-cliの方が都合が良かったこともあり、無事切り替えが決定されました。
ありがとう!Webpacker!そして安らかに....††Webpacker††
明日のパーソンリンクアドベントカレンダー、引き続き私が担当です(`・ω・´)
明日はパーソンリンク内のナレッジ共有の立ち上げと利用ツールについて語ります!
それでは。