webpackのbundle後のJavaScriptのサイズを減らしている話

webpackのbundle後のJavaScriptのサイズを減らしている話

はじめに

リクルートテクノロジーズに4月に新卒入社した 辻 健人です.GitHubではmaxmellonで活動しています. 7月より,やりとりも作成もラクになるシフト管理サービス「Airシフト」
のエンハンス開発を担当しています. 以前は,React製SPAのパフォーマンスチューニング実例という内容で記事を書きました. 今回は同じSPAにおいて,いかにwebpackが生成するJavaScriptのバンドルサイズを減らすかについて紹介していきます.

webpackが,そもそも何のためのツールか,バンドルする理由などについては割愛させていただきます. そういった話は,こちらの記事 (Webpack の考え方について – mizchi’s blog) がわかりやすいと思います.

Airシフトのアーキテクチャ

Airシフトは,React-Reduxで開発されており,かつSSRを行っておりコードの大半がクライアントとサーバーサイドで同じもの(isomorphic)を利用しています.

isomorphicにしている理由は,楽にサーバーサイドレンダリングを行うためです. SSRをしている理由はこちらのスライドYou need to know SSRにあるようにFirst View Performanceを大切にしているために実施しています.

AirシフトにおけるJavaScriptの構成

AirシフトのJavaScriptの構成は,redux-plutoというボイラープレートをforkして開発しています. redux-pluto および,Airシフトでは,react-rotuer (v3)faceyspacey/react-universal-component と dynamic import と hint コメントを組み合わせたwebpackのchunkを分ける手段 (code-splitting) を利用して,各locationごとにJavaScriptのchunkを分けています. また,分けられたchunkは,そのlocationに初めてアクセスするときに取得されるようになっています.

 

そもそも,今まで考えなしにJavaScriptのバンドルサイズを肥大化させていたのと,それに対して歯止めをかけていないという問題があります. その問題に対しての考え方に Peformance Budgets というものがあります. 参考: Start Performance Budgeting

Performance Budgets は何かと言うと,簡単にいうと利用していい最大のJavaScriptのサイズを予算として捉えてどれだけ使うか考えるというもの, つまりチームが超過してはならないJavaScriptのbundle sizeを設定することです.

気をつけなければ行けないのが,Performnce Budgets は単なるしきい値ではなく,意味のあるものでなければいけません. 例えば,Fast 3G 回線で Lighthouce で 80点を出すために,170KB などというものです.

Lighthouceというのは,google性のパフォーマンスやSEO,PWAに重点を置いた監査ツールで,ChromeのDevtools上のAuditから利用することが可能です.

現状をしらべる

まず,いままで一切 Performance Budgets を意識していなかった,Airシフトですが,どれくらい予算を超過しているか調べてみましょう. webpackには,bundleした結果を分析するためのjsonを吐き出すpluginがあります. (stats-webpack-plugin)

このプラグインを,production build用のwebpack.configに追記してbuildすることにより,stats.jsonが生成されます.

 

stats.jsonだけでは,直感的にどこの何が重いのかを調べることが出来ません.そこで,可視化用ツールである webpack-bundle-analyzer を利用します.

次のコマンドを実行することで利用が可能です.

すると,サーバーが立ち上がり,webpackのchunkの内訳を見ることが出来ます.

ひとまず,main.jsがとても大きいのでここに焦点を当てて容量を減らしていくことにします. 実際にmain.jsがどれくらいの多きさかは,カーソルを合わせることで確認できます

容量にして529KBです.特に 次のjsが大部分を占めている事がわかります.

  • moment-timezone の latest.json
  • joi-browser.js
  • react-dom.production.min.js
  • lodash
  • bluebird.js

この中から削れるものを順番に削っていきます

実際に減らしていく

moment-timezone の latest.json を減らす

まずは,このファイルがなんのために存在しているのかを知るために中身を見てみましょう

 

中身を見てみると,タイムゾーンの情報が,600行に渡って定義されていることがわかります. jsonは,uglifyが出来ない(短い変数名などに置き換えて圧縮できない)ので非常に大きな容量を占めていという結果になっていると考えれます.

gzipされたあとのサイズで20KBもあるので非常に大きいです.

そもそも,Airシフトは現時点では,現時点では必要ないので,Asia/Tokyoのタイムゾーンさえあれば,他の情報は不要です. なので,これをwebpackのpluginを使ってAsia/Tokyoのjsonのみ定義されたファイルに差し替えましょう. 対応方法は,moment-timezone/issues/356 に掲載されていました.

まず,Asia/Tokyoのみを定義した latest.json を用意します

次に,webpackに標準で入っている,NormalModuleReplacementPlugin を用いて,latest.json を差し替えます

これで,bundle後のJavaScriptを見てみると,

before after

と,latest.json が見えないほど小さくなったことがわかります 容量にして20KB近く減らすことが出来ました

bluebird を削る

bluebirdもかなり大きくて,Gzipした状態で20KB近くを占めるライブラリです. 機能としては,高速なPromise兼polyfillです.

Promiseのpolyfillはbabel-polyfillでやっているので blurbird はそもそも不要と考えました. そこで,package.json を見て npm コマンドで bluebird を削除しようとしたのですが,そもそもpackage.jsonにblurbirdはありませんでした つまり,依存先の依存先にbluebirdが存在すると推測できます.

依存先にそのモジュールがあるかどうかを調べるには,npm ls が便利です.今回の場合,次のコマンドを実行しました. 依存先の依存先の依存先の… までしっかり見たいので,depthを雑に大きめの数字に設定しておきます.

npm ls の詳細のオプションは,ドキュメントを参照してください

調べてみると,then-sleep というライブラリが,native-or-bluebird というものに依存しており,そのモジュールが bluebird に依存している事がわかりました.

then-sleep は

みたいな,await可能なsleepを提供しているだけだったので,自分でサクッと書き直しました.

これにより,依存モジュールからbluebirdを削除することが出来ました.実際に減ったか確認してみましょう.

before after

無事,blurbird がなくなりました.

joi-browser を削る

そもそもjoi-browserは,何かと言うとバリデーションライブラリのjoiのブラウザ用のjsです. なぜこんなに容量が大きくなっているかというと,バリデーションのエラーメッセージがi18n対応のために,非常に豊富に用意されているからです. これは,i18n対応をしているサービスでは非常に便利ですが,Airシフトは今のところ日本語のみをサポートしているので,i18nで容量が膨らんでしまっていることが不本意でした.容量にすると,なんと43KBです.

そこで,バリデーション用のライブラリを完全に別のものに置き換えることにしました.置き換える先のライブラリは,favalidにしました. favalidの特徴としては,

  • 機能が少なく軽量
  • 関数型ライクなAPI
  • エラーメッセージを出力するインターフェースのみを定義しておりエラーメッセージを持たない

このライブラリの方針が,本サービスとマッチしている感じて採用しました.

詳しくは,フロントエンド向けvalidator: favalidの紹介を参照してください.

joiを置き換えるときは,既存のバリデーションに対してvalid/invalidの全パターンをテストコードで定義して,そのテストコードがpassするように置き換えました. 置き換えた例のコードをすこし紹介します.

joiで書かれた次のようなバリデーターは

favalidで書くとこのようになります

これによりどれだけ減ったかを見てみましょう

before after

joi-browser がなくなったことが確認できました.

babel-polyfill から polyfill.io に変える

そもそもbabel-polyfillは,IE11を始めとしたレガシーなブラウザで不足している機能を,レガシーなブラウザで動くコードで再実装して 同じように動かせるというものです.(例えば,Array.prototype.includesPromise)

容量を見てみると,28KB になります

ただ,モダンなブラウザではこれらは不要です.そこで,便利なpolyfillを配布しているサービス,polyfill.ioを利用します.

polyfill.ioとは,User-Agentを見て配布するpolyfillを変えてくれるというものです. モダンなブラウザで,アクセスすると空になり,レガシーなブラウザでアクセスすると,フルで入っているみたいなものです. モダンなブラウザでアクセスしたときのみですが,取得するJavaScriptの容量を大きく減らせます.

before after

モダンブラウザのみですが,28Kb減らすことが出来ました.

今後の課題

これまでまとめてきた施策を案件開発の合間合間で行ってきました.機能も日に日に増え,ビジネスコードもましているので, 減らした分純粋には減っていませんが,現状で 100KB ほど減らすことが出来ました.(polyfill.io に関しては,いま時点ではまだ対応されたものがリリースされていません)

before after

今後やっていきたいものとしては,

  • Autolinker はもう少しミニマル要件のものを作り直す
  • lodashはどうにかしてTree-Shaking出来ないか調査する
  • moment を date-fnsなど別ライブラリに変える

というのがあります.lodashに関しては,依存先の依存先のlodashとかも絡んできていてかなり問題が複雑な現状があります.

react-dom.production.min.js が大きい問題は,React16.x のロードマップでモダナイズして小さくするというのがあったので期待しています.

現状,パフォーマンスバジェットをまもるというよりは,収まるように目指している状況ですが,これから bundle後のJavaScriptのサイズを定期的に観測していき,不要なライブラリを削ぎ落として行きたいと考えています.

この改善が直接的に業務価値を与えるとは思いませんが,こういった細々とした点を改善していくことが,よりよりプロダクトを作る上で大事だと思います. 特に,Airシフトの場合,忙しい店長が使うサービスなので,時間に関わるものについてはシビアに捉えていきたいです.

これをきっかけに,まずbundleされたjsがどれほどの大きさか調べてみてはいかがでしょうか?