MAS-359 フロントエンド バンドル分割最適化(singlefile 脱却・React.lazy + manualChunks)
1. 概要・目的
1.1 概要表
| 項目 | 内容 |
|---|---|
| 案件 ID | MAS-359 |
| カテゴリ | 🔧 Infra (フロントエンドビルド最適化) |
| Phase | P2 |
| 優先度 | ★★★ (MAS-356 Tremor 採用・MAS-357 Firebase 移行の前提条件) |
| 所要時間 | Phase 1: ~2 日 / Phase 2: ~3 日 (MAS-357 と並行) |
| 対象ファイル (変更) | webapp_client/vite.config.tswebapp_client/src/CockpitApp.tsxwebapp_client/src/cockpit-main.tsxwebapp_client/src/multiyear-main.tsxwebapp_client/scripts/sync-engines.mjs |
| 前提案件 | MAS-356 Tremor + v0.dev 採用 (問題の直接的な契機) |
| 後続案件・連携余地 | MAS-357 Firebase Hosting 移行 (Phase 2 の実施条件) / MAS-358 Cockpit 左サイドバー (バンドル圧迫を緩和してから実装) |
1.2 背景・課題
発生経緯
MAS-356 で Tremor (@tremor/react) + Tailwind CSS v4 を導入した結果、financial_cockpit.html のバンドルサイズが 364 KB → 1,200 KB (+836 KB) に増大した。
| ファイル | MAS-356 前 | MAS-356 後 | 増加 |
|---|---|---|---|
financial_cockpit.html | 364 KB | 1,200 KB | +836 KB |
multiyear_cockpit.html | 290 KB | 322 KB | +32 KB |
sidebar_spa_shell.html | 179 KB | 179 KB | 0 KB |
根本原因
GAS HtmlService の CSP 制約により vite-plugin-singlefile で全 JS/CSS を 1 HTML にインライン化している。inlineDynamicImports: true の強制により Vite の標準コード分割 (manualChunks / dynamic import) が無効化されており、Tremor の依存ライブラリ (shadcn/ui + tailwind-merge) が全量バンドルに入る。
financial_cockpit.html の内訳 (推定)
├── React 18 + React DOM ~130 KB
├── Tailwind CSS (purged) ~30 KB
├── Tremor + shadcn/ui + deps ~600 KB ← 問題の主因
├── chart.js + d3-sankey ~150 KB
├── GAS エンジン ×8 ~100 KB
└── アプリコード (13 panels) ~190 KB
なぜ「ファイル分割」か
パネルが増えるほど cockpit は肥大化し続ける(MAS-358 サイドバー追加でさらに増加見込み)。Tremor を使い続けるためにはバンドルを複数ファイルに分割して総量を分散させる必要がある。
1.3 目標サイズ
| フェーズ | cockpit | multiyear | sidebar | 手段 |
|---|---|---|---|---|
| 現状 | 1,200 KB | 322 KB | 179 KB | — |
| Phase 1 後 | 600 KB 以下 | 322 KB | 179 KB | tree-shaking 最適化 + 未使用削除 |
| Phase 2 後 | 200 KB 以下 | 150 KB 以下 | 100 KB 以下 | Firebase + 本格コード分割 |
2. Phase 1 — GAS 上での最適化 (singlefile 継続)
2-A. Tremor の tree-shaking 効果測定
現在 Tremor は named import を使用しており、原理上 tree-shaking が効く。ただし Tremor v3 の内部依存 (Recharts など) が side-effect で引き込まれていないか確認する。
# ビルド前後のサイズ比較
npm run build:cockpit
ls -la ../templates/financial_cockpit.html
# Vite のバンドル解析
VITE_BUILD_TARGET=cockpit npx vite build --reportCompressedSize 2>&1 | grep KB
確認ポイント: Recharts (~300 KB) が含まれていないか。Badge / Callout / Card / Metric / ProgressBar は非チャートコンポーネントのため、Recharts が除外されればそれだけで ~300 KB 削減できる。
2-B. 未使用コンポーネントの削除
CockpitApp.tsx でインポートされているが現在の画面に表示されていないコンポーネントを確認・削除する。
| コンポーネント | 状況 | 対処 |
|---|---|---|
InsightPanel.tsx | MAS-057 v2.5 で AiSuggestPanel に統合済み → 不使用の可能性 | import 削除対象確認 |
ThreeSectionTable.tsx | 同上、削除済みの可能性 | import 削除対象確認 |
FiveYearChart.tsx | v2.5 で削除済みの可能性 | import 削除対象確認 |
# CockpitApp.tsx で import されているが JSX に登場しないコンポーネントを検出
grep -n "import {" src/CockpitApp.tsx
grep -n "<InsightPanel\|<ThreeSectionTable\|<FiveYearChart" src/CockpitApp.tsx
2-C. エンジンファイルのエントリ分離
cockpit-main.tsx に 8 つのエンジン副作用 import が集中しているが、一部は multiyear のみで必要なものがある。エントリ別に必要最小限のエンジンのみ import する。
| エンジンファイル | cockpit | multiyear | 備考 |
|---|---|---|---|
442_social_insurance_tier_engine.js | ○ | ○ | 両方必要 |
443_corporate_housing_optimizer.js | ○ | × | cockpit のみ |
444_per_diem_policy_engine.js | ○ | × | cockpit のみ |
445_required_revenue_solver.js | ○ | ○ | 両方必要 |
447_capital_allocation_engine.js | ○ | × | cockpit のみ (MAS-355) |
449_dividend_mix_optimizer.js | ○ | × | cockpit のみ |
451_multiyear_planner.js | ○ | ○ | 両方必要 |
454_compensation_strategy_engine.js | ○ | × | cockpit のみ |
multiyear-main.tsx から cockpit 専用エンジンを削除することで multiyear のサイズ削減が見込める。
2-D. Phase 1 判定
npm run build:cockpit
ls -la ../templates/financial_cockpit.html
| 結果 | 判断 |
|---|---|
| 600 KB 以下 | ✅ Phase 1 完了。Firebase 移行まで運用継続 |
| 600〜900 KB | ⚠️ 一定の改善。Phase 2 (Firebase) を優先スケジューリング |
| 900 KB 超 | ❌ tree-shaking が効いていない。Tremor の import 方法を再検討、または Tremor を CapitalAllocationPanel のみに限定 |
3. Phase 2 — Firebase Hosting + 本格コード分割
前提: MAS-357 Phase 1 (Firebase Hosting への移行) 完了後に実施。vite-plugin-singlefile を削除し、Vite 標準のコード分割に戻す。
3-A. vite.config.ts の変更
// Before (GAS singlefile)
plugins: [react(), tailwindcss(), viteSingleFile()],
build: {
rollupOptions: {
output: { inlineDynamicImports: true },
},
assetsInlineLimit: 100_000_000,
cssCodeSplit: false,
}
// After (Firebase 標準ビルド)
plugins: [react(), tailwindcss()],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
'vendor-tremor': ['@tremor/react'],
'vendor-charts': ['chart.js', 'd3-sankey'],
'engines': [
'./src/engines/442_social_insurance_tier_engine.js',
'./src/engines/443_corporate_housing_optimizer.js',
// ... 他エンジン
],
},
},
},
cssCodeSplit: true,
}
3-B. React.lazy() によるパネル遅延ロード
// Before
import { CapitalAllocationPanel } from './cockpit/CapitalAllocationPanel';
import { SoloFinancialStatementsPanel } from './cockpit/SoloFinancialStatementsPanel';
// After — スクロールで見えるまでロードを遅延
const CapitalAllocationPanel = React.lazy(
() => import('./cockpit/CapitalAllocationPanel')
);
const SoloFinancialStatementsPanel = React.lazy(
() => import('./cockpit/SoloFinancialStatementsPanel')
);
// JSX 側
<Suspense fallback={<div className="panel-skeleton" />}>
<CapitalAllocationPanel ... />
</Suspense>
優先度の高い遅延ロード候補: 重量パネルから適用する。
| パネル | 推定サイズ | lazy 候補 |
|---|---|---|
CapitalAllocationPanel | 大 (Tremor 使用) | ✅ 最優先 |
CompensationStrategyPanel | 大 (Tremor 使用) | ✅ |
SoloFinancialStatementsPanel | 大 | ✅ |
ApproachComparisonPanel | 中 | ✅ |
ScenarioBar / GuardrailBanners | 小 (初期表示必須) | ✗ |
3-C. エントリポイント別ビルドの維持
Phase 2 以降も 3 エントリ構造は維持する。Firebase Hosting では各 HTML が CDN から配信され、共有 chunk (vendor-react.js など) はブラウザキャッシュに乗る。
Firebase Hosting
├── financial_cockpit.html ~50 KB (HTML シェル + 初期 chunk)
├── multiyear_cockpit.html ~50 KB
├── sidebar_spa_shell.html ~50 KB
└── assets/
├── vendor-react.xxxx.js ~130 KB ← 3 ページ共通キャッシュ
├── vendor-tremor.xxxx.js ~600 KB ← cockpit 初回のみ、以降キャッシュ
├── vendor-charts.xxxx.js ~150 KB
├── engines.xxxx.js ~100 KB
├── cockpit.xxxx.js ~100 KB
└── cockpit.xxxx.css ~30 KB
2 回目以降のアクセスでは vendor-react / vendor-tremor はキャッシュ済みになり、実効ダウンロードは ~100 KB 程度に収まる。
4. システムアーキテクチャ (ファイル構成・変更箇所)
4.1 Phase 1 変更ファイル
| ファイル | 変更内容 |
|---|---|
webapp_client/src/CockpitApp.tsx | 未使用 import の削除 |
webapp_client/src/cockpit-main.tsx | cockpit 専用エンジンのみに整理 |
webapp_client/src/multiyear-main.tsx | multiyear 専用エンジンのみに整理 |
webapp_client/scripts/sync-engines.mjs | エントリ別エンジンリストを管理できるよう整理 |
4.2 Phase 2 変更ファイル (MAS-357 完了後)
| ファイル | 変更内容 |
|---|---|
webapp_client/vite.config.ts | viteSingleFile() 削除・manualChunks 追加・cssCodeSplit: true |
webapp_client/src/CockpitApp.tsx | React.lazy() + Suspense 適用 |
webapp_client/src/MultiyearApp.tsx | 同上 |
webapp_client/package.json | vite-plugin-singlefile 削除 |
300_ui/302_spa_bridge.js | HtmlService.createHtmlOutputFromFile → Firebase URL 参照に変更 (MAS-357 と共同) |
5. エッジケース・異常系
| # | 条件 | 検知方法 | 期待される挙動 |
|---|---|---|---|
| 1 | Phase 1 後も 900 KB 超 | ls -la templates/financial_cockpit.html | Tremor を CapitalAllocationPanel のみに限定し CompensationStrategyPanel は既存 CSS に戻す |
| 2 | React.lazy() 適用後に Suspense fallback が長く表示される | ブラウザ DevTools の Network タブ | chunk サイズを確認し、Suspense の boundary 粒度を調整 |
| 3 | manualChunks で循環依存が発生 | Vite ビルドエラー Circular dependency | 依存グラフを確認し chunk 境界を調整 |
| 4 | Firebase 移行後に GAS 側の HtmlService 呼び出しが残存 | GAS 実行時エラー | MAS-357 と連携してルーティングを Firebase URL に切り替え |
| 5 | vendor-tremor.js が CDN キャッシュされず毎回 600 KB ダウンロード | Network タブで Cache-Control ヘッダー確認 | Firebase Hosting の firebase.json で Cache-Control: max-age=31536000 を設定 |
6. テスト要件
Phase 1 検証
# 1. ビルド成功確認
npm run build:cockpit && npm run build:multiyear && npm run build:sidebar
# 2. サイズ確認
ls -la ../templates/*.html
# 3. 既存機能の動作確認 (dev サーバーで)
VITE_BUILD_TARGET=cockpit npm run dev
# → 全パネルが正常表示されること
# → CapitalAllocationPanel の 3 バケツ・ROE・Rule of 40 が表示されること
Phase 2 検証
# 1. チャンク分割確認
npm run build:cockpit 2>&1 | grep "dist/"
# → vendor-react / vendor-tremor / engines チャンクが生成されること
# 2. Firebase ローカルエミュレーター確認
firebase emulators:start --only hosting
# → 2 回目アクセス時に tremor chunk がキャッシュされること (Network: from cache)
7. 推奨実行モデル
| ステップ | モデル | 理由 |
|---|---|---|
| Phase 1-A tree-shaking 計測 | Haiku | コマンド実行・ファイルサイズ確認のみ |
| Phase 1-B 未使用 import 削除 | Sonnet | JSX 内の実際の使用箇所を横断確認する判断が必要 |
| Phase 1-C エンジン分離 | Sonnet | cockpit / multiyear 両エントリの依存関係を追う必要あり |
| Phase 2 vite.config 変更 | Sonnet | manualChunks の設計判断 |
| Phase 2 React.lazy() 適用 | Sonnet | Suspense boundary の粒度判断 |
8. 受け入れ条件
Phase 1
-
financial_cockpit.htmlが 600 KB 以下 - 既存パネルの表示が変化しない(CapitalAllocationPanel・CompensationStrategyPanel 含む)
-
multiyear_cockpit.htmlが 300 KB 以下
Phase 2
-
financial_cockpit.html(HTML シェル) が 100 KB 以下 -
vendor-react.jsが cockpit / multiyear / sidebar 3 エントリで共有される - 2 回目アクセス時のネットワーク転送量が 200 KB 以下 (キャッシュ効果確認)
-
React.lazy()適用パネルが Suspense fallback を経て正常表示される
9. 依存関係
- MAS-356: Tremor 導入がバンドル増大の起点。MAS-359 Phase 1 が完了してから MAS-356 Phase 3 (v0.dev 活用) を進めると効率的
- MAS-357: Phase 2 の前提条件。Firebase Hosting への移行が完了してから Phase 2 に着手する
- MAS-358: Cockpit ナビサイドバー追加はさらにバンドルを圧迫するため、Phase 1 完了後に実装推奨
10. 変更履歴
| 日付 | バージョン | 変更内容 |
|---|---|---|
| 2026-05-03 | v1.0 | 初版起票。MAS-356 Tremor 導入後の 1,200 KB 問題を受け、Phase 1 (GAS 上の最適化) / Phase 2 (Firebase + 本格コード分割) の 2 段階解決策を設計 |