freeeプロジェクト管理でJavaScriptバンドルサイズ削減に取り組んだ話

こんにちは! 中部オフィスでエンジニアをやっているichienです。

5月に入社してfreeeプロジェクト管理の開発を担当していました。
今回はfreeeプロジェクト管理のJS(JavaScript)バンドルサイズを削減した話を紹介します。

改善前はバンドルしたJSファイルが5MB以上に肥大化しておりダウンロードに数秒かかっていました。 その分、ファーストビューの表示もその分遅くなり、ユーザ体験に課題がありました。

今回は次の3つの施策を実施して、5MB1.7MBまで削減し、ダウンロード時間も70%短縮できた話をします。

  • gzip圧縮の適応
  • webpack-bundle-analyzerで現状の可視化と削減対象の決定
  • 不要な日付・祝日データの排除

また、今回の取り組みは一度にすべて対応したわけではありません。次の様にstep-by-stepで進めることを意識しました。

  1. 現状を可視化し理解する
  2. 着目点を決める
  3. 小さな対応を多くやる

では、次から実施内容を紹介していきます!

JSファイルのgzip圧縮

巨大バンドルサイズの原因を探るため、Chrome DevToolsのNetworkタブを確認しました。 バンドルファイルのレスポンスヘッダーにcontent-encoding:gzip がなかったことが、問題特定の決め手になりました。

freeeプロジェクト管理はRuby on Rails+NGINXの構成で稼働しており、JSファイルなどのコンテンツも配布しています。

Railsはデフォルトでコンテンツの圧縮は行わないが、NGINXのngx_http_gzip_moduleを利用することでgzip圧縮を適応することができます。
プロダクトのコードでは、既に次の形で設定がありました。

  • 修正前
# gzip圧縮を有効化
gzip on;  
# gzip圧縮する対象のコンテンツタイプを指定
gzip_types application/javascript text/javascript;  

解決策は漏れていた設定追加だった

解決策はgzip_typesapplication/x-javascriptを追加しただけです。

NGINXのモジュールで利用しているpassengerがJSファイルをapplication/x-javascriptのコンテンツタイプで配信していました。 これに対する設定漏れが原因でした。

修正後のnginx.conf.erbは以下となります。

  • 修正後
gzip on;  
# application/x-javascriptを追加
gzip_types application/x-javascript application/javascript text/javascript;  

原因特定の流れ

原因特定のため、私が辿っていったコードの流れを紹介しておきます。

  • nginx.config.erbのhttpブロックでpassengerのhttp.erbの読み込みを確認。
http {
  <%= include_passenger_internal_template('http.erb', 2) %>
  • http.erb2行目mime.typesの読み込みを確認。
include '<%= PhusionPassenger.resources_dir %>/mime.types';
  • 最後に、mime.types8行目でjsファイルとcontent-typeの対応を確認。
types {
    application/x-javascript  js;

webpack-bundle-analyzerで現状の可視化と削減対象の決定

次に、更に削減可能な箇所を特定するためwebpack-bundle-analyzerを使ってバンドルされているモジュールサイズの可視化を行いました。

webpack bundle analyzerの結果
webpack bundle analyzerの結果

今回はファイルサイズが巨大なJSONファイルに着目し、以下のライブラリのファイル削減方法を紹介します。

date-holidaysの祝日情報を日本のみに限定

freeeプロジェクト管理では、日付ごとに工数入力できる機能があります。 その日の祝日判定にdate-holidaysを使っています。

その中で447KBもあるholidays.jsonを削減する方法を調査しました。

holidays.jsonのファイルサイズ
holidays.jsonのファイルサイズ

調べてみると、このholidays.jsonは世界の祝日情報を持っていました。 プロダクトの要件として日本の祝日情報が判定できれば良いので、日本だけに限定できれば大幅削減可能と考えられます。

ライブラリ側で対象国を絞り込めるholidays2jsonが用意されています。

これをインストール時にnpm post scriptで実行する形を取りました。 これでnpm installyarn install完了後に自動で実行され、データが削減された状態になります。

"scripts": {
    "postinstall": "holidays2json --pick JP --min"
}

対応前後のholidays.json単体で比較すると、444kb44kb となり大幅に小さくできました。

日本に限定したholidays.jsonのファイルサイズ
日本に限定したholidays.jsonのファイルサイズ

moment-timezoneも日本のデータに絞る

moment-timezoneに含まれるlatest.jsonのファイルサイズも186KBとかなり大きいです。

latest.jsonのファイルサイズ
latest.jsonのファイルサイズ

こちらも日本の情報に絞ることでファイルサイズを削減します。

対象国を絞るには、moment-timezone-data-webpack-pluginが利用できます。
webpackの設定例は以下となります。

const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin');
module.exports = {
    plugins: [
        new MomentTimezoneDataPlugin({
            matchCountries: 'JP'
        }),
    ],
};

結果、moment-timezone自体のデータサイズが186KB17KBとなり、約1/10に削減できました。

日本に限定したmoment-timezoneのファイルサイズ
日本に限定したmoment-timezoneのファイルサイズ

まとめ

紹介した3つの方法で、バンドルサイズを5MB1.7MBまで削減でき、ファイルダウンロード時間も70%短縮できました。

lighthouseのPerformanceスコアを2859まで改善することができました。

改善前 改善後
改善前のlighthouseのPerformanceスコア 改善後のlighthouseのPerformanceスコア

自分が貢献できる箇所を探し出し、小さな改善を複数行い、最終的に大きなインパクトを作り出すことができました。
振り返ると、分析と可視化をちゃんとステップに含め、成果を修正前後で比較してわかりやすい形にできたのが良かったと考えています。

改善はもちろんこれで終わりではありません。継続して課題の可視化と解決に取り組んで行きます! 最後まで読んでいただきありがとうございました。