SPAで巨大バンドルが生まれる理由とTree Shakingの限界

巨大バンドルの原因は「ライブラリが重い」ではありません

SPA(Single Page Application)でパフォーマンスが悪化すると、よく「このライブラリが重い」「Reactが重い」という話になります。
しかし実際の現場で計測すると、原因の多くは特定のライブラリではありません。

結論から言うと、SPAで巨大バンドルが生まれる本当の理由は
“アプリケーションの構造”がバンドルに反映されてしまうからです。

そして、その解決策としてよく語られるTree Shakingには明確な限界があります。

なぜSPAはバンドルが1つに集まるのか

従来のMPA(Multi Page Application)では、ページごとにJavaScriptが分かれていました。

  • /login → login.js
  • /products → products.js
  • /admin → admin.js

一方SPAではルーティングがブラウザ内で行われます。
つまり最初のアクセス時に「全ルートの可能性」を読み込む設計になります。

ここで問題が発生します。

例えば、次のような構成です。

  • 商品一覧ページ
  • 管理画面
  • グラフ表示
  • エディタ機能

ユーザが最初に開いたのがログイン画面でも、ビルドされたJavaScriptには管理画面用コードも含まれます。
これが巨大バンドルの出発点です。

Tree Shakingとは何をしているのか

Tree Shakingは「使っていないコードを削除する最適化」です。
ES Modulesの静的解析により、importされていない関数を除去します。

import { add } from './math.js'

この場合、math.jsに100個関数があってもaddしか残りません。

ここまでは正しい理解です。
しかしここに大きな誤解があります。

Tree Shakingはファイル単位の最適化ではないという点です。

Tree Shakingが効かないパターン

副作用(side effect)

import './initAnalytics'

このようなコードは削除できません。
なぜなら、何が実行されるか解析できないためです。

ライブラリの多くは初期化処理を持っています。
その時点でTree Shakingは止まります。

CommonJS

const lib = require('library')

CommonJSは動的読み込みです。
ビルド時に使用箇所を特定できないため、丸ごと含まれます。

動的参照

obj[methodName]()

これも解析不能です。
バンドラは安全のため削除しません。

つまりTree Shakingは「静的に証明できる不要コード」しか消せません。

ライブラリが巨大化する理由

モダンフロントエンドでは、1つのライブラリが複数の機能を持ちます。

  • UIコンポーネント
  • 国際化
  • 日付処理
  • バリデーション

例えば日付ライブラリをimportすると、ロケール情報やパーサまで入ることがあります。
実際にはformatしか使っていなくてもです。

import dayjs from 'dayjs'

この1行が数十KBになる場合もあります。

さらに大きな原因:共有依存

SPAの巨大バンドルの最大要因は、実はアプリ側のコードです。

例:

  • 共通レイアウトがグラフコンポーネントをimport
  • グラフがchartライブラリをimport
  • すべてのページがchartを読み込む

すると、ログイン画面でもグラフライブラリが含まれます。

つまり問題はライブラリではなく依存関係の方向です。

コードスプリッティングの役割

巨大バンドルの対策はTree Shakingではなくコードスプリッティングです。

const AdminPage = React.lazy(() => import('./AdminPage'))

これにより、管理画面のコードはアクセス時に初めて読み込まれます。

ここで初めて「ルート単位」の最適化が行われます。

Tree Shaking:不要な関数を削除
Code Splitting:不要な機能を読み込まない

役割が違います。

よくある誤解

「Webpackの設定を変えれば軽くなる」という考えは半分しか正しくありません。
バンドルサイズはビルドツールより、コンポーネント設計の影響の方が大きいです。

特に注意すべきは共通モジュールです。

  • utilsに何でも入れる
  • index.tsで全部export
  • barrel export

この構成は全ページに依存を広げます。

export * from './components'

この書き方は便利ですが、Tree Shakingを妨げる場合があります。

実際に起きる問題

巨大バンドルは単に読み込みが遅くなるだけではありません。

  • JSパース時間増加
  • コンパイル時間増加
  • メインスレッドブロック
  • LCP悪化

回線速度より、ブラウザの実行時間がボトルネックになります。

特にモバイル端末では顕著です。

対策の優先順位

効果が高い順です。

  • ルート単位のコードスプリッティング
  • 管理画面の分離
  • 重いライブラリの遅延ロード
  • Tree Shaking最適化

Tree Shakingは最後です。
削減量は数KB〜数十KBに留まることが多いです。

まとめ

SPAのパフォーマンスは、ビルド設定やフレームワークよりも依存設計に左右されます。
巨大バンドルは「ツールの問題」ではなく「構造の問題」です。

Tree Shakingは重要な最適化ですが、万能ではありません。
使っていないコードを消すことはできますが、使う可能性のあるコードは消せません。

SPAを軽くする本質は、コードを削ることではなく必要なときまで読み込まない設計です。
どれだけ小さくするかより、どれだけ遅らせるかが体感速度を決めます。