bitA Tech Blog
🟢

Vueアプリの初期表示を4倍速く!2.3MBのSPAがCode splittingでパフォーマンス改善するか検証した実例

に公開

SPA(Single Page Application)では、規模が大きくなってくるとJavaScriptファイルのバンドルサイズが肥大化し、初期ロード時間が長くなる問題がよく発生します。

私が担当している業務アプリケーションのプロジェクトでも、2.3MBもの巨大なJSファイルが生成され、特に低速ネットワーク環境などでロードに時間がかかっていました。

Slow 4G環境でのパフォーマンス分析

Chromeの開発者ツールでSlow 4G環境をシミュレーションした結果を分析します。総ロード時間は約39.7秒とかなり長い結果となりました。

カテゴリ 時間 割合 説明
System 441 ms 1.1% ブラウザのシステム処理に費やされた時間
Scripting 340 ms 0.9% JavaScriptの実行に費やされた時間
Rendering 3 ms 0.01% レイアウト計算とDOMレンダリングに費やされた時間
Loading 1 ms ≈0% リソースのロードに費やされた時間
Messaging 0 ms 0% イベント処理などのメッセージングに費やされた時間
Painting 0 ms 0% 画面描画に費やされた時間
Total 39,664 ms 100% 総所要時間

この記事では、2.3MBの大きなバンドルファイルのSPAに、コード分割(Code Splitting)を実施するとどれくらいパフォーマンスが改善するのか検証しました。フレームワークはVue3でViteでビルドしています。

実行環境

Vue 3.3.8
Vite 7.1.4

この記事で触れること

  • 巨大なJSバンドルがパフォーマンスに与える影響
  • Code Splittingを実装する簡単な方法
  • 実際のプロジェクトでの改善効果(最大74%の高速化)
  • さらなる最適化のためのmanualChunks設定

パフォーマンス改善の効果

lighthouseで、Code Splitting実施有無で画面表示時にどこまでパフォーマンス改善できるか
計測した結果、改善前と改善後で以下のような違いが見られました。

指標 改善前 改善後 高速化率
First Contentful Paint 10.3s 2.8s 約3.7倍向上
Largest Contentful Paint 20.2s 5.2s 約3.9倍向上
Total Blocking Time 110ms 50ms 2.2倍向上
Speed index 10.7s 3.2 約3.3倍向上

First Contentful Paint (FCP)
最初のコンテンツ描画時間:ブラウザがページ読み込みを開始してから、テキストや画像など最初の要素がユーザーに表示されるまでの時間。10.3秒から2.8秒に改善され、ユーザーが「何かが表示された」と感じるまでの待ち時間が大幅に短縮されました。

Largest Contentful Paint (LCP)
最大コンテンツ描画時間:ビューポート内で最も大きな要素(通常はメインコンテンツ)が表示されるまでの時間。20.2秒から5.2秒に改善され、ユーザーが「主要コンテンツが表示された」と感じるタイミングが大幅に早くなりました。ページの体感的な読み込み完了時間を示す重要な指標です。

Total Blocking Time (TBT)
総ブロッキング時間:FCPから完全インタラクティブになるまでの間で、メインスレッドがブロックされている合計時間。110ミリ秒から50ミリ秒に改善され、ページがより早く操作可能になったことを示します。ユーザーがボタンをクリックしたりフォームに入力したりできるようになるまでの応答性に関係します。

Speed Index
表示速度指標:ページコンテンツがどれだけ速く視覚的に表示されるかを示す指標。10.7秒から3.2秒に改善され、ページ全体の読み込み進行が視覚的にどれだけ速くなったかを表します。ユーザーが「ページが徐々に表示される様子」をどれだけ早く体験できるかを測定しています。

コード分割(Code Splitting)の方法

Code Splitting を実施するには、以下のように createRouter() で routes オプションに渡している各コンポーネントを動的インポートへ変更します

import { createRouter, createWebHistory } from 'vue-router';

import { createRouter, createWebHistory } from 'vue-router';
- import Dashboard from './views/Dashboard.vue'
+ const Dashboard = () => import('./views/Dashboard.vue');

export const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'dashboard',
      component: Dashboard,
    }
  ]
});

ビルドファイル比較

改善前

dist/index.html                              0.75 kB │ gzip:   0.43 kB
dist/assets/xxxxxx.svg                       4.77 kB │ gzip:   1.91 kB
dist/assets/xxxxxx.svg                       5.43 kB │ gzip:   2.11 kB
dist/assets/index-C0sgKXYF.css             546.35 kB │ gzip:  67.06 kB
dist/assets/index-lWCrEY9g.js            2,373.92 kB │ gzip: 621.56 kB

バンドルされたJavaScriptファイルが1つの巨大なチャンクとなっていることが分かります。
通信は以下のような流れになります。

改善後

Code Splitting実施後に、ビルドしてみると、JavaScriptは5つのバンドルファイルに分散され、
また合計のファイルサイズも小さくなっていることが分かります。

dist/index.html                            0.75 kB │ gzip:   0.43 kB
dist/assets/xxxxxx.svg                     4.77 kB │ gzip:   1.91 kB
dist/assets/xxxxxx.svg                     5.43 kB │ gzip:   2.11 kB
dist/assets/index-2Fjgx3l7.css            53.76 kB │ gzip:   6.69 kB
dist/assets/index-9fxDF9tW.css           111.00 kB │ gzip:  20.76 kB
dist/assets/index-BXNsI0cd.js              0.17 kB │ gzip:   0.16 kB
dist/assets/index-BWLQkOB6.js              0.24 kB │ gzip:   0.20 kB
dist/assets/index-BwKVYz1Q.js              0.47 kB │ gzip:   0.33 kB
dist/assets/index-zZ0SqPAu.js            626.83 kB │ gzip: 205.53 kB
dist/assets/index-CUoZjIKI.js            670.65 kB │ gzip: 209.22 kB

通信の流れは以下のようになります。

初回アクセス時:

  • Dashboard.jsチャンクがダウンロードされる
  • ブラウザのキャッシュに保存される

2回目以降のアクセス:

  • ブラウザはDashboard.jsのURLをキャッシュと照合
  • ファイル名(ハッシュ)が同じなら、キャッシュから読み込み
  • ネットワークリクエストは発生しない

これにより、ユーザーはアプリケーションをより速く操作でき、特に低速ネットワーク環境や頻繁にアクセスするユーザーにとって大きなメリットとなります。

manualChunks

また、vite.config.jsrollupOptions の manualChunks
モジュールを vender-xxxx でまとめることにより、関連するコードを同じチャンクにまとめることができ、キャッシュ効率を向上させることができます。

  rollupOptions: {
    output: {
      manualChunks: {
        'vendor-vue': ['vue', 'vue-router', 'vuex'],
        'vendor-cytoscape': ['cytoscape'],
        'vendor-utils': ['axios', 'lodash']
      }
    }
  }
dist/index.html                            0.75 kB │ gzip:   0.43 kB
dist/assets/xxxxxx.svg                     4.77 kB │ gzip:   1.91 kB
dist/assets/xxxxxx.svg                    5.43 kB │ gzip:   2.11 kB
dist/assets/index-2Fjgx3l7.css            53.76 kB │ gzip:   6.69 kB
dist/assets/index-9fxDF9tW.css           111.00 kB │ gzip:  20.76 kB
dist/assets/index-BQ9F1k9q.js              0.17 kB │ gzip:   0.16 kB
dist/assets/index-Cf9wf5fK.js            626.83 kB │ gzip: 205.53 kB
dist/assets/index-DqOi9xjr.js            670.61 kB │ gzip: 209.21 kB

今回のケースでは、manualChunks設定ありの場合、小さなチャンクのファイル数は少なくなっていますが、大きなチャンクのサイズはほぼ同じでパフォーマンス上の大きな違いはないことが分かりました。

まとめ

Code Splittingは実装コストが比較的低い割には効果が高い最適化手法ですが、有効なケースと向かないケースがあるようです。潜在的な問題点としては、ウォーターフォール型リクエストの発生や、ルート遷移時のちらつき、デバッグの難しさなどが挙げられるため、メリット・デメリットを考慮した上で実装するかは検討する必要があります。

Code Splittingが有効なケース

  • 低速ネットワーク環境でのアクセスが多い場合
  • モバイルデバイスからの利用が多いサービスの場合
  • 複数の独立した機能を持つ合計バンドルサイズが大きい場合

Code Splittingが向かないケース

Code Splittingは多くの場合でパフォーマンス向上に役立ちますが、すべての状況に適しているわけではありません。以下に不向きなケースと潜在的な問題点を説明します。

  • 合計バンドルサイズが小さい(500KB未満など)場合、分割による恩恵が少ない場合
  • 複数タブを頻繁に切り替える管理画面など高頻度で同時に使われる機能がユーザーセッション中に短時間で使用される場合
  • コンポーネント間の依存関係が複雑で、分割が難しい場合
  • リアルタイム取引システムなど遷移後の操作性が即時性を要求される場合
bitA Tech Blog
bitA Tech Blog

Discussion