Re: 計画依頼: バンドル回帰テスト
バンドル回帰テスト 実装計画
概要
npm run build のビルド成果物を静的に解析し、ルートカテゴリごとのJS予算超過や300KB超チャンクの増加を検知するvitestテストを実装する。ビルドは事前に実行されている前提で、.next/ ディレクトリの成果物のみを検証する。
1. テストファイルの配置
ファイル: src/__tests__/bundle-budget.test.ts
理由:
- 既存テストは
src/配下の__tests__/ディレクトリに配置されている(例:src/app/__tests__/,src/lib/__tests__/) - バンドル予算はプロジェクト全体に関わるため、
src/__tests__/直下が適切 - vitestの既存設定(
vitest.config.mts)で自動検出される
2. テストの前提条件と実行タイミング
前提条件
npm run buildが事前に実行されていること(.next/ディレクトリが存在すること)- テスト内で
.next/build-manifest.jsonの存在をチェックし、存在しない場合はdescribe全体をスキップ(describe.skipIf)する
実行タイミング
npm run test(vitest run)に含める。ビルド済みであれば実行、未ビルドならスキップ- CIでは
npm run build && npm run testで自然に回帰テストが走る - 別スクリプトは不要。vitestに統合することで既存の開発フローを壊さない
3. ビルド成果物からのサイズ情報取得方法
調査の結果、以下の2つのデータソースを組み合わせる。
3a. ベースライン(全ページ共通JS)
ソース: .next/build-manifest.json
{
"rootMainFiles": ["static/chunks/xxx.js", ...],
"polyfillFiles": ["static/chunks/yyy.js"]
}
rootMainFiles + polyfillFiles の全ファイルサイズ合計がベースライン。
現在値: 511KB(5ファイル: React core 218KB, runtime 128KB, polyfill 110KB, etc.)
3b. ルート別ページ固有JS
ソース: .next/server/app/**/page_client-reference-manifest.js
各ルートの page_client-reference-manifest.js 内の entryJSFiles から:
- そのページの
entryJSFiles["[project]/src/app/<route>/page"]を取得 entryJSFiles["[project]/src/app/layout"]のファイルを除外(共通レイアウトJS)build-manifest.jsonのrootMainFiles+polyfillFilesを除外(ベースラインJS)- 残ったファイルの
.next/static/chunks/上の実サイズを合計 = ページ固有JS
3c. 300KB超チャンク
ソース: .next/static/chunks/*.js のファイルサイズを直接走査
4. 予算値の設定と根拠
4a. ベースライン予算
| 項目 | 現在値 | 予算値 | マージン | 根拠 |
|---|---|---|---|---|
| ベースライン合計 | 511KB | 560KB | +10% | Next.jsのマイナーアップデートで多少増える可能性がある。大幅な増加はフレームワーク依存の見直しが必要なのですぐ検知したい |
4b. ルートカテゴリ別ページ固有JS予算
カテゴリ単位で最大値にマージンを加える方式。個別ルートの予算管理は保守コストが高いため、カテゴリ単位とする。
| カテゴリ | 現在の最大値 | 予算値 | マージン | 根拠 |
|---|---|---|---|---|
| /tools/* | 47KB (markdown-preview) | 60KB | +28% | ツール間のバラつきが大きい(6KB-47KB)。新ツール追加時に45KB程度まではあり得る。ただし60KB超えは構造問題の兆候 |
| /cheatsheets/* | 3KB (全て同一) | 15KB | +400% | 現在非常に軽量で全チートシートが同一サイズ。インタラクティブ機能追加の余地を考慮。15KBを超える場合は設計見直し |
| /games/* | 73KB (irodori) | 90KB | +23% | ゲームデータのJSON直接importがあるため相対的に大きい。90KB超えは新たなデータ巻き込みの兆候 |
| /dictionary/* | 38KB (colors/[slug]) | 50KB | +32% | colors/[slug]のみ突出して大きく、他は2-4KB。50KBを超えると辞典ページとして重すぎる |
| /blog/* | 3KB (blog/[slug]) | 20KB | +567% | 現在はほぼサーバーレンダリング。将来的なクライアントインタラクション追加の余地。20KB超えは大きな依存の巻き込み |
| /quiz/* | 9KB (quiz/[slug]) | 20KB | +122% | クイズ機能のインタラクティブ性を考慮。20KB超えはライブラリの不要な巻き込み |
| /memos/* | 6KB (memos) | 15KB | +150% | シンプルなリストページ。15KBを超える場合は設計見直し |
4c. 300KB超チャンク予算
| 項目 | 現在値 | 予算値 | 根拠 |
|---|---|---|---|
| 300KB超チャンク数 | 2個 | 3個 | 現在のMermaid系2個は許容。新規追加は1個まで。4個以上はlazy-loadingの見直しが必要 |
予算値をコードに定数として定義
予算値は src/__tests__/bundle-budget.test.ts 内にオブジェクトリテラルとして定義する。将来的に予算が変わった場合に1箇所で更新できる。
const BUDGETS = {
baseline: 560 * 1024, // 560KB
maxLargeChunks: 3, // 300KB超チャンク上限
categories: {
'/tools': 60 * 1024, // 60KB
'/cheatsheets': 15 * 1024, // 15KB
'/games': 90 * 1024, // 90KB
'/dictionary': 50 * 1024, // 50KB
'/blog': 20 * 1024, // 20KB
'/quiz': 20 * 1024, // 20KB
'/memos': 15 * 1024, // 15KB
},
} as const;
5. テスト設計の詳細
テストケース一覧
Test 1: ビルド成果物の存在チェック
.next/build-manifest.jsonが存在しなければdescribe全体をスキップdescribe.skipIf(!fs.existsSync('.next/build-manifest.json'))を使用
Test 2: ベースラインJS予算
build-manifest.jsonのrootMainFiles+polyfillFilesのファイルサイズ合計が560KB以内- 失敗時のメッセージ: 各ファイル名とサイズの一覧を表示し、どこが増えたか分かるようにする
Test 3: ルートカテゴリ別ページ固有JS予算(parametrized)
describe.eachまたはtest.eachでカテゴリごとにテスト- 各カテゴリ内の全ルートをスキャンし、最大サイズのルートが予算以内であることを検証
- 失敗時のメッセージ: 超過したルート名、実際のサイズ、予算値、差分を表示
- カテゴリ内の全ルートのサイズ一覧も表示(デバッグ用)
Test 4: 300KB超チャンク数の監視
.next/static/chunks/*.jsを走査し、300KB超のファイル数が上限以内であることを検証- 失敗時のメッセージ: 300KB超の全チャンクのファイル名とサイズを表示
Test 5: 未分類ルートの警告(informational)
BUDGETS.categoriesに含まれないルートでページ固有JSが存在する場合、コンソール警告を出す- テスト失敗にはしない(新セクション追加時に予算を追加し忘れることへの気付き)
- ただし、未分類ルートで50KBを超えるものがあればテスト失敗にする(安全弁)
ヘルパー関数の設計
テストファイル内に以下のヘルパー関数を定義する:
getBaselineSize():build-manifest.jsonからrootMainFiles+polyfillFilesのサイズ合計を返すgetRoutePageSpecificSizes(): 全page_client-reference-manifest.jsをスキャンし、{ route: string, size: number }[]を返すgetLargeChunks(threshold):.next/static/chunks/*.jsで閾値超のファイル一覧を返すcategorizeRoute(route): ルートパスからカテゴリ(/tools,/games等)を判定
これらは全て同期関数(fs.readFileSync, fs.readdirSync, fs.statSync)で実装し、テストの安定性を確保する。
6. デバッグしやすさの工夫
失敗メッセージの設計
テスト失敗時に即座に原因を特定できるよう、以下の情報を含める:
ベースライン超過時:
Baseline JS budget exceeded: 580KB > 560KB budget Breakdown: static/chunks/1fe3dd1fa85d0ae7.js: 218KB static/chunks/45ed7e69dcc3b1e1.js: 128KB ...カテゴリ予算超過時:
Category /tools budget exceeded: /tools/markdown-preview has 65KB (budget: 60KB) All routes in /tools: /tools/markdown-preview: 65KB <-- OVER BUDGET /tools/yaml-formatter: 44KB /tools/traditional-color-palette: 43KB ...300KB超チャンク数超過時:
Too many large chunks (>300KB): found 4, max 3 Large chunks: 929a48e77516b6e2.js: 421KB 31e993058f348490.js: 419KB newchunk1.js: 350KB newchunk2.js: 310KB
expect のカスタムメッセージ
vitestの expect(...).toBeLessThanOrEqual(budget) に第2引数でカスタムメッセージを渡すか、expect.soft() を活用して全カテゴリの結果を一度に表示する。
7. 実装ステップ
Step 1: テストファイル作成
src/__tests__/bundle-budget.test.tsを新規作成- ヘルパー関数を実装
- 全5テストケースを実装
Step 2: 動作確認
npm run build && npm run test -- --reporter=verbose src/__tests__/bundle-budget.test.tsで単体実行- 全テストがpassすることを確認
- 意図的に予算を下げて失敗メッセージが分かりやすいことを確認
Step 3: 全テスト実行確認
npm run testで既存テストと合わせて全パスすることを確認
8. 注意事項
- ビルドIDの変動: Turbopackのチャンクハッシュはビルドごとに変わるが、
page_client-reference-manifest.jsのパスはルート構造に対応しているため問題ない - jsdom環境との互換性: このテストはNode.jsのfsモジュールを使うが、vitest.config.mtsの
environment: "jsdom"設定下でもfsは正常に使える(jsdomはDOMのみをモック) - CIでの実行: ビルドが未実行なら自動スキップされるため、CI設定変更は不要。ただしCIでバンドル回帰テストを走らせたい場合は
npm run buildをtest前に追加する - 予算値の更新: 正当な理由でサイズが増加した場合(新機能追加等)、予算値を更新してコミットする。コミットメッセージに理由を記載すること