1. はじめに
エンジニアになる前、個人開発をしてたアプリを久しぶりにビルドしたところ、立ち上がりが死ぬほど遅いことに気づきました😭
当時は特に気にしていなかったのですが、改めて触ってみると開発体験が悪すぎて「これが毎回続くのはかなりストレスだな」と痛感。
せっかくなので、原因を突き止めてしっかり改善までやってみようと思いました。
具体的には以下の流れで進めました。
- Webpack の設定見直し
- Vite の導入
- Vuetify / Firebase のバンドルサイズ軽量化と読み込み最適化
2. プロジェクトの状況
今回改善を進める前の環境は、ざっくりこんな感じでした。
- Vue 2(vue@2.6系)
- Vue Router
- Vuex
- Vuetify(マテリアルデザインUI)
- Firebase(Auth / Firestore / Storage)
- Vue CLI(Webpack 4系ベース)
機能としては、チャット・プロフィール管理・ユーザー認証などを備えた中規模のSPAで、個人開発にしてはわりと盛り込んだ作りになっています。
とりあえず、状況を把握するために
npm run build -- --report
を実行して、ビルドのどのくらいの時間がかかっているのか、各バンドルの構成を確認。
🐘 主な問題点
⚠️ Warning:ファイルサイズの警告
1. アセットサイズの警告
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
Webpack の推奨サイズ(244KB)を超える静的ファイルに対する警告。
例:
- フォントファイル:1.25MB(かなり大きい)
-
chunk-vendors.js
:1.34MB(主に依存ライブラリ)
chunk-vendorsで、各バンドルのサイズがめちゃくちゃデカそう…
2. エントリーポイントサイズの警告
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB).
Entrypoints:
app (2.13 MiB)
- 最初に読み込まれるエントリポイント(app.js)の合計が 2.13MB
- 初期読み込みが遅くなる可能性がある
各コンポーネントに関しても、かなり大きそうなことが判明…
3. Material Design Icons のフォントファイルが巨大
fonts/materialdesignicons-webfont.ttf 1.25 MiB
fonts/materialdesignicons-webfont.woff 574 KiB
- すべてのフォーマット(ttf, woff, eot, woff2)が含まれている
- 実際に使用しているアイコンはごく一部
analyzerで、可視化してみるとこんな感じ
firabaseとvuetifyの占める割合がえぐい。
当時は Vue CLI のデフォルト設定でとりあえず動かしていたのですが、Firebase と Vuetify のコンポーネントをほぼ丸ごと読み込んでいて、chunk-vendors.js に全部詰め込まれている状態。
- ビルド時間が 13〜14秒
- 開発サーバーの初期起動が 10秒以上
と、今考えるとめちゃくちゃ待たされる状態で、ちょっとした修正でも毎回このスピード感はきついなと感じるレベルでした。
業務だとそういうのを改善してくれる人がいて、先人の肩の上に乗ってただけなんだろうなということをつくづく思いました笑
3. 改善フェーズ①:Webpack構成の見直し
とりあえず既存のVue CLI(Webpack)構成で、改善できそうな点は見つけたので、まずはその辺から改善していきます。
- コード分割(Lazy Loading)
- ルート単位で
import()
による遅延ロードを実装 - 初期ロードを軽くして、画面遷移時に必要なファイルを後から読み込む形に
- 今回の場合、初期表示で必須ではない機能が多かったので、ほぼ全てのコンポーネントに適用しました
- ルート単位で
※こちらは一例
```jsx
// router/index.js
const routes = [
{
path: '/feature',
component: () => import(/* webpackChunkName: "feature" */ '../views/Feature.vue')
}
]
```
機能別チャンク生成
| チャンク名 | サイズ |
| --- | --- |
| `survey.0cccc828.js` | 43.75 KiB |
| `room.fcb121b1.js` | 19.98 KiB |
| `users.7ab2ca80.js` | 16.09 KiB |
| `auth.ba243ec2.js` | 8.14 KiB |
| `chat.e15e82ec.js` | 2.55 KiB |
| `room-list.06e5ebcd.js` | 1.56 KiB |
- コンポーネントごとにチャンクを分割
- 1ファイルあたりのサイズを大幅に小さくした -
Vuetifyの最適化(コンポーネント&アイコン)
- 必要なものだけ読み込む
-
Firebaseの軽量化
- Firebaseは、全モジュールを一括importしていたため、初回のチャンクが重くなっていた
- 今回の対応で、モジュールを必要なものだけに分割
正直、上記取り組みではビルド時間の短縮にそこまで効果はありませんでした。
ただ、chunkサイズはかなり削減され、ベンダーチャンクのサイズが約33%削減。
項目 | 最適化前 | 最適化後 | 削減量 / 差分 |
---|---|---|---|
chunk-vendors.js | 1.32 MiB(= 約1353 KiB) | 913.10 KiB | 🔻 約440 KiB(約33%減) |
これは、Viteを導入したら期待できるのでは…!
4. 改善フェーズ②:Vite移行
Webpackでの最適化では限界がありましたが(やり方悪い説はある)、まあとりあえずざっくりでもビルド時間が早くなればいいやということで、Viteを導入した時の検証をしてみました。
Viteを選んだ理由としては
- 圧倒的に早い初期起動
- HMR(ホットモジュールリプレース)のレスポンスの良さ
といったモダンな仕組みの魅力があります。
Vite移行にあたっては、
- vue-cli プラグインから vite-plugin-vue2 へ切り替え
- Firebaseのモジュールインポートを個別に書き換え
- rollup-plugin-visualizer でのチャンク分析
-
manualChunks
設定で Firebase と Vuetify を分割
などを調整しました。
これらの対応により、ビルド時間は約13〜14秒 → 8〜9秒まで短縮できました👏
まだ、firebaseの依存モジュールが大きな塊として残っているので最後にこれを最適化していきます!
5. 改善フェーズ③:Firebaseの最適化
-
Firebaseのインポートを動的インポート(dynamic import)に変更
- ルートごとに必要になったときだけ読み込む
- これにより最初のバンドルからFirebaseを除外できる¥
- Vite の
manualChunks
設定も併用してチャンク分離
具体例
従来はこうだったものを
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
↓
動的インポートに書き換え
const { initializeApp } = await import('firebase/app')
const { getAuth } = await import('firebase/auth')
const { getFirestore } = await import('firebase/firestore')
結果
-
Firebaseのコードが初期チャンクから完全に分離
-
必要になったときだけ読み込む構成にしたため
ビルド完了時間が約8秒 → 6秒前後にさらに短縮🎉
-
Firebase を動的インポートに切り替えたことで、初期ロードには含めず必要になったタイミングでだけロードできるようになりました
📊 chunk-vendors.js のサイズ推移
フェーズ | サイズ | 削減率 / コメント |
---|---|---|
改善前(Vue CLI デフォルト) | 約1.32 MiB (1353 KiB) | |
Webpack構成最適化後 | 約913 KiB | 🔻 約33%削減 |
Vite移行 + Firebase動的インポート後 | 約250 KiB(gzip後 約70〜80 KiB) | 🔻 約80%削減(初期ロードに影響しないレベル) |
🕒 ビルド実行時間の推移
フェーズ | ビルド時間 |
---|---|
改善前(Vue CLI デフォルト) | 約13〜14秒 |
Webpack構成最適化後 | 約8〜9秒 |
Vite移行後 | 約6〜8秒 |
Firebaseを動的インポート化後 | 約6秒前後(最安定) |
振り返り
今回、初めて改善系のタスクを実施しました。普段アプリケーションを触っているとそこまで意識しない部分だったので、新たな視点が増えたと思います。各コンポーネントを分離させることで、1つのファイルサイズを小さくすることで、ビルド時に実行が早くなったり、Lazy Loadingや動的インポートを使用すれば、ビルド時には含めなくていいなど行動を起こしてなかったら気づけなかったことがたくさんありました!
依存を無計画に一括importなどは、バンドルの肥大化にも繋がるのでこれからは気をつけようと思います(笑)
また進め方として、依存の棚卸し→コード分割→ツール移行→動的インポートという筋道で段階的にリファクタリングする楽しさを味わえたのもよかったです!
おまけ
ビルド実行時間の短縮を達成したのも束の間、firebaseのライブラリをかなり色々削減したこともあり、ログイン周りでログインのバグが発生!!!
とりあえず、cursorと相談しながら進めたら初期の状態に戻すことはできたのでよかったですが、これが業務であったときはちゃんと影響範囲やこなすタスクの順番、粒度などを考えないと下手したらシステムが動かなくなるなと本当に思いました💦そういう意味でもやってよかったタスクだったとおもいます!