はじめに
こんにちは、あるいはこんばんは。フロントエンドエンジニアの id:kasaharu です。
Web アプリケーションにユーザーがアクセスしてきたとき、ユーザーはブラウザを介して HTML / CSS / JavaScript といった多くの assets をダウンロードすることが多いのではないでしょうか? フロントエンド技術の進化に伴い、ユーザーがダウンロードする assets ファイルの数は増え、またファイルひとつひとつのサイズも増加しています。
ファイル数やサイズの増加はユーザーがそのアプリケーションを使えるようになるまでの時間が増えることにもつながるため望ましくありません。 遅延ロードを使って初期バンドルのサイズを小さくする方法を取ることもありますが、日頃から意識して開発していないとついつい初期バンドルに含まれてしまいます。
assets ファイルの数やそのサイズの増加によるユーザビリティ低下を検知するために、この記事では初期バンドルのサイズを監視する方法を提案します。
assets サイズと budget
弊社ではフロントエンド開発に Angular を使用していますが、Angular には budget を設定して、assets サイズを監視する仕組みがあります。 例えばビルドのオプションで次のように設定すると、初期バンドルが 500kB を超えたときに警告となり 1MB を超えるとエラーとして通知するようになります。
{ … "configurations": { "production": { … "budgets": [ { "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" }, ] } } }
Angular CLI や Nx を使ってアプリケーションを作成していれば始めから設定されているため、特に気にすることはありません。 あとは CI でビルドの workflow を組んでおけば、workflow が動くたびにチェックすることができます。
しかし、実際はあまりうまく運用されていないのではないでしょうか?警告が発生しても CI は pass するため、おそらく問題に気づくのはエラーになったときであることが多いです。エラーになるときはたいてい別の作業をやっているときであるため、その回避方法は「一旦 budget のサイズをあげる」になりがちです。
この問題を解決するには定期的に assets サイズを監視し、増加傾向に気づく仕組みが必要だと考えます。
何を監視するか
仕組みを構築するために、監視対象を決めます。 通常、ビルド時に以下のようなログが出力されます。
Initial chunk files | Names | Raw size | Estimated transfer size chunk-W6XF7GTY.js | - | 184.72 kB | 45.79 kB chunk-Y3RVIEGX.js | - | 152.54 kB | 44.99 kB chunk-Z4LUB37C.js | - | 120.37 kB | 20.66 kB styles.css | styles | 96.47 kB | 8.50 kB main.js | main | 67.65 kB | 16.78 kB polyfills.js | polyfills | 33.10 kB | 10.72 kB chunk-IX6G3U3V.js | - | 4.10 kB | 1.03 kB chunk-BE7JP4VI.js | - | 2.16 kB | 845 bytes | Initial total | 661.10 kB | 149.28 kB Lazy chunk files | Names | Raw size | Estimated transfer size chunk-MITEH7FN.js | lazy-chunk-a | 145.49 kB | 24.77 kB chunk-KQQ6B2D5.js | browser | 62.22 kB | 16.53 kB chunk-HQTDUG32.js | lazy-chunk-b | 5.86 kB | 1.63 kB
ログを見ると Initial chunk files
と Lazy chunk files
のふたつに大別されていることがわかります。 今回はすべてのユーザーに影響がある初期バンドルを監視することにします。
つまり Initial chunk files
に分類されているファイルが対象です。
上のログを例にしたとき、次のサイズの推移が追えることがゴールとなります。
- main.js
- polyfills.js
- styles.css
- chunk x 5 の合計
- この chunk ファイルは開発者が明示的に遅延ロード指定で分割したものではなく Angular がよしなに分割しており、これらは初期化時にロードされる
- これらのファイルの合計
どうやって監視するか
弊社ではアプリケーションの監視に New Relic を使っています。すでに自動テストの code coverage の推移を New Relic に溜めたりしておりノウハウもあります。
参考: New Relic でフロントエンドのモニタリングダッシュボードを作ってみた - EmotionTechテックブログ
そこで、assets ファイルのサイズも同じように日次で New Relic に送ることにします。 最終的に New Relic へのデータ送信はスクリプト経由でおこなうという前提で、まずは必要な情報を集めます。
送りたいデータは決まっているものの、出発地点がコンソールに出力されているログだとパースが大変であるため、まずはビルドログを JSON で出力することにします。
先ほどログに出力されていた情報は --stats-json
というビルドオプションをつけることで stats.json という JSON ファイルで出力できます。 弊社では Nx を使っているため、具体的には次のコマンドを実行します。
npm run nx run my-app:build -- --stats-json --source-map=false --output-hashing=none
一緒に付与した source-map
と output-hashing
について簡単に説明します。 source-map
はソースマップを出力するかどうかのオプションで、今回は出力しないようにしています。これは単純に今回の処理に不要だからです。次に output-hashing
ですが、こちらは assets ファイルにハッシュを付与するかどうかのオプションです。今回はハッシュをつけない設定にしています。ハッシュをつけてしまうとビルドのたびにファイル名が変わってしまい stats.json のパース処理が複雑になるため、それを避けるために none
を指定しています。
この設定でビルドを実行すると dist/apps/my-app/ に stats.json が出力されます。 JSON ファイルの構成を簡単に説明します。まず第一階層には inputs
と outputs
のふたつがあります。今回はビルド成果物の情報が欲しいので outputs
にのみフォーカスします。
{ "outputs": { "main.js": { "imports": [ { "path": "chunk-BE7JP4VI.js", "kind": "import-statement" }, { "path": "chunk-MITEH7FN.js", "kind": "dynamic-import" } ], "entryPoint": "apps/my-app/src/main.ts", "bytes": 69276 }, "chunk-BE7JP4VI.js": { "bytes": 2208 }, "chunk-MITEH7FN.js": { "entryPoint": "apps/my-app/src/app/lazy-chunk-a/lazy-chunk-a.ts", "bytes": 157548 }, "polyfills.js": { "entryPoint": "angular:polyfills:angular:polyfills", "bytes": 33898 }, "styles.css": { "entryPoint": "angular:styles/global:styles", "bytes": 98781 } } }
outputs
直下には生成されたファイル名がキーとして並んでいます。例えば main.js
を見ると、この中の bytes
の値が 69276 となっており、コンソールに出力されていた 67.65 kB と一致することがわかります。
つまり stats.json から main.js のファイルサイズを取得するには outputs → main.js → bytes
を見ると良いことがここから読み取れます。 JSON から必要なデータを取得するツールとして、今回は GitHub Actions でも使用可能な jq を使います。jq を使った取り出しをする場合は次のコマンドを実行します。
jq '.outputs["main.js"].bytes' stats.json
polyfills.js と styles.css のサイズはこれと同じ要領で取り出せるため、コマンドは割愛します。
次に chunk ファイルの取り出し方を考えます。JSON ファイルを見ると Initial chunk files
と Lazy chunk files
の両方の情報が並列に並べられており、ファイル名(キー名)からはどれが対象のファイルか判断がつきません。そこで main.js
を経由して必要な情報を取り出します。 main.js
の imports
には kind
というデータがあり、その名前からこれが chunk ファイルの種類だということがわかります。kind
が import-statement
になっているものが遅延ロードされていないもの、つまり Initial chunk files
に含まれるファイルになります。
chunk ファイルはひとつひとつのサイズではなく、その合計だけを監視対象とすると決めました。 そこで、少し複雑ですが次のコマンドで取り出します。
jq '[.outputs as $outputs | $outputs["main.js"].imports[] | select(.kind == "import-statement") | .path as $path | $outputs[$path].bytes] | add' stats.json
これで必要なデータが揃いました。
New Relic に送る
New Relic に送るために次のようなスクリプトを用意しました。
send () { APP_NAME=$1 MAIN=$(jq '.outputs["main.js"].bytes' dist/apps/${APP_NAME}/stats.json) POLYFILLS=$(jq '.outputs["polyfills.js"].bytes' dist/apps/${APP_NAME}/stats.json) STYLES=$(jq '.outputs["styles.css"].bytes' dist/apps/${APP_NAME}/stats.json) OTHER_CHUNK=$(jq '[.outputs as $outputs | $outputs["main.js"].imports[] | select(.kind == "import-statement") | .path as $path | $outputs[$path].bytes] | add' dist/apps/${APP_NAME}/stats.json) SUM=$(($MAIN + $POLYFILLS + $STYLES + $OTHER_CHUNK)) curl --json @- -H "Api-Key:$NEWRELIC_LICENSE_KEY" "https://insights-collector.newrelic.com/v1/accounts/$NEWRELIC_ACCOUNT_ID/events" << EOS { "appName": "$APP_NAME", "eventType": "$EVENT_TYPE", "main.js": "$MAIN", "polyfills.js": "$POLYFILLS", "styles.css": "$STYLES", "chunkFiles": "$OTHER_CHUNK", "sum": "$SUM" } EOS }
送った結果がこちらです。
弊社は Nx で複数のアプリケーションを管理しているので、アプリケーションごとに一週間の推移が見えるようにダッシュボードを作成しました。 app1 がここ数日でも増えており、すぐに効果を実感しています。
おわりに
いかがでしたか? このグラフを使った改善については今後の課題になると思いますが、まずは改善に向けた材料が手に入りました。 この取り組みの是非については、機会があればいつか紹介したいと思います。
エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。 プロダクトに興味のある方、Angular を使ったアプリケーション開発をしたい方、ぜひ採用ページからご応募をお願いいたします。