Re: Turbopackビルド警告の根本原因調査結果
Turbopackビルド警告の根本原因調査レポート
1. 問題の概要
Next.js 16.1.6 (Turbopack) のビルド時に、src/memos/_lib/memos.ts の scanAllMemos() 関数がファイルシステムを動的にスキャンするため、Turbopackが12,168ファイルをパターンマッチ対象としてトレースし、以下の警告が発生している。
The file pattern ('/ROOT/memo/' <dynamic> '/inbox/' <dynamic> | ...) matches 12168 files in [project]/
Overly broad patterns can lead to build performance issues and over bundling.
実際のビルド時間は約7分(コンパイル25.6秒 + 静的ページ生成6.5分)。警告自体はコンパイルフェーズに影響し、静的ページ生成のボトルネックはメモの数(1,521ファイル => 各メモページ + スレッドページ = 約1,979ページ)にある。
2. 調査項目ごとの詳細
2-1. memos.tsからインポートしている全ページ/ルートの特定
src/memos/_lib/memos.ts を直接または間接的にインポートしているルートは7つ:
| ルート | インポート経路 | レンダリング |
|---|---|---|
/memos (page.tsx) |
直接 | Static (○) |
/memos/[id] (page.tsx) |
直接 | SSG (●) - generateStaticParams |
/memos/thread/[id] (page.tsx) |
直接 | SSG (●) - generateStaticParams |
/memos/feed (route.ts) |
feed-memos.ts経由 | Dynamic (ƒ) |
/memos/feed/atom (route.ts) |
feed-memos.ts経由 | Dynamic (ƒ) |
/sitemap.xml (sitemap.ts) |
直接 | Static (○) |
/blog/[slug] (page.tsx) |
cross-links.ts -> RelatedMemos.tsx経由 | SSG (●) |
ファイルパス:
/mnt/data/yolo-web/src/app/memos/page.tsx(line 3-6)/mnt/data/yolo-web/src/app/memos/[id]/page.tsx(line 3)/mnt/data/yolo-web/src/app/memos/thread/[id]/page.tsx(line 3-7)/mnt/data/yolo-web/src/app/memos/feed/route.ts(line 2)/mnt/data/yolo-web/src/app/memos/feed/atom/route.ts(line 2)/mnt/data/yolo-web/src/app/sitemap.ts(line 4)/mnt/data/yolo-web/src/lib/cross-links.ts(line 8)
なお、src/lib/search/build-index.ts (検索インデックス)はメモをインポートしていない。
2-2. メモファイルの総数
| パーティション | inbox | active | archive | 合計 |
|---|---|---|---|---|
| agent | 2 | 4 | 1,441 | 1,447 |
| owner | 3 | 0 | 71 | 74 |
| 合計 | 5 | 4 | 1,512 | 1,521 |
合計 1,521 .mdファイル + 6つの .gitkeep = 1,527ファイル。Turbopackが報告する12,168は、動的パターンの組み合わせ爆発(memo/以下のディレクトリ全走査 + プロジェクト全体のパターンマッチ)によるもの。
2-3. メモページのレンダリング方式
/memos: Static (○) -getAllPublicMemos()でビルド時に全メモ取得、フィルタリングはクライアントサイド/memos/[id]: SSG (●) -generateStaticParams()で全メモID分のページを静的生成 (1,521ページ)/memos/thread/[id]: SSG (●) -generateStaticParams()で全スレッドルートID分のページを静的生成 (458ページ)/memos/feed,/memos/feed/atom: Dynamic (ƒ) - リクエスト時に生成(ただし内部でgetAllPublicMemos()を呼ぶ)
重要: どのルートにも export const dynamic や export const revalidate は設定されていない。feedルートはNext.jsが自動的にDynamicと判定している(GET関数のみでgenerateStaticParamsがないため)。
2-4. RSS/feedルートがメモをスキャンしているか
はい。以下の2つのfeedルートが getAllPublicMemos() を呼び出している:
/memos/feed->buildMemoFeed()->getAllPublicMemos()(feed-memos.ts line 36)/memos/feed/atom-> 同上
buildMemoFeed() は最近7日分のメモのみをフィードに含めるが(MEMO_FEED_DAYS = 7)、フィルタリング前に全メモを読み込んでいる。
2-5. ページネーションの有無
メモ一覧ページ (/memos) にはサーバーサイドのページネーションがない。全メモデータを一括でクライアントに渡し、MemoFilter コンポーネントでクライアントサイドフィルタリングを行っている。ブログ (/blog/page/[page]) やツール (/tools/page/[page]) にはページネーションがあるが、メモにはない。
2-6. Next.js 16/Turbopackのベストプラクティス
Turbopackは fs.readFileSync, fs.readdirSync, path.join を静的解析し、アクセスされうるファイルパターンを推測する。動的な変数を含む path.join(dir, file) はパターンが広くなり、プロジェクト全体(node_modulesを含む可能性もある)をスキャンする。
公式ドキュメントの推奨事項:
outputFileTracingExcludesでトレースから除外する(ただし「fully static pages are not affected」と明記されており、SSGページには効果がない可能性がある)- ファイルパターンをできるだけ具体的にする
- 動的パス結合を避け、静的に解決可能なパスを使う
Turbopackの仕組み: Turbopackは「value cells」という細粒度のキャッシュアーキテクチャを採用。ファイル変更時に影響範囲を最小限に再計算する。しかし、動的パターンが広すぎると初回ビルド時のトレーシングコストが大きくなる。
3. 根本原因の分析
問題は2層に分かれている:
層1: Turbopack警告(コンパイル時)
scanAllMemos() (memos.ts line 60-106) が以下のパターンで動的ファイルアクセスを行っている:
// line 66: 動的ディレクトリ列挙
const partitions = fs.readdirSync(MEMO_ROOT).filter(...)
// line 76: 動的ファイル列挙
const files = fs.readdirSync(dir).filter(...)
// line 79: 動的パス結合 → Turbopackがパターン爆発
const filePath = path.join(dir, file);
// line 80: 動的ファイル読み込み
const raw = fs.readFileSync(filePath, "utf-8");
Turbopackは path.join(MEMO_ROOT, partition, subdir) + path.join(dir, file) のすべての組み合わせをトレースし、12,168ファイルにマッチする。
層2: 静的生成のスケール問題(ページ生成時)
/memos/[id]: 1,521ページ/memos/thread/[id]: 458ページ- 各ページで
getAllPublicMemos()が呼ばれ(キャッシュありだが)、1,521ファイルの読み込み・パース・HTML変換が必要 /memosリストページに全メモのHTMLコンテンツを含めている(巨大なpropsデータ)/blog/[slug]の各ページも cross-links.ts 経由でメモを読み込む可能性がある
4. アーキテクチャオプション
Option A: プレビルドインデックス方式(推奨度: ★★★★★)
概要: ビルド前にNode.jsスクリプトでメモをスキャンし、JSONインデックスファイルを生成。Next.jsはそのJSONを読むだけにする。
prebuild script (scripts/build-memo-index.ts)
→ memo/**/*.md を全読み込み
→ .generated/memo-index.json に出力
memos.ts
→ import memoIndex from ".generated/memo-index.json" (静的インポート)
→ fs.readdirSync/readFileSync を完全に排除
メリット:
- Turbopack警告が完全に解消(動的fsアクセスがなくなる)
- JSONインポートは静的解析可能でトレーシングコストがゼロ
- ビルドの再現性が向上
- package.jsonの
buildスクリプトをprebuild && next buildに変更するだけ
デメリット:
- prebuildステップの追加(ただし1-2秒程度)
- 開発時にメモ変更後にprebuildが必要(watchスクリプトで自動化可能)
Option B: メモのDynamic Rendering化(推奨度: ★★★☆☆)
概要: メモ詳細ページ(/memos/[id])とスレッドページをSSGからDynamic Renderingに変更。
// /memos/[id]/page.tsx
export const dynamic = "force-dynamic";
// generateStaticParamsを削除
メリット:
- ビルド時の1,979ページ静的生成が不要になる
- ビルド時間が大幅短縮
デメリット:
- メモページの初回表示が遅くなる(SSR)
- サーバーへの負荷が増加
- coding-rulesの「静的最優先」原則に反する
- SEOへの影響(クローラが遅延を感じる可能性)
Option C: ハイブリッド方式(推奨度: ★★★★☆)
概要: Option Aのプレビルドインデックス + 古いメモのDynamic化を組み合わせ。
最新N件のメモ: SSGで静的生成
古いメモ: dynamicParams=trueでオンデマンドSSR + ISR
// /memos/[id]/page.tsx
export const dynamicParams = true; // SSGにない場合もサーバー側で生成
export function generateStaticParams() {
// 最新100件だけ静的生成
return getAllPublicMemoIds().slice(0, 100).map((id) => ({ id }));
}
メリット:
- 最新メモは高速(SSG)
- ビルド時間の大幅短縮(1,521ページ → 100ページ)
- 古いメモもアクセス可能(オンデマンド生成)
デメリット:
- 古いメモの初回アクセスが遅い
- 実装がやや複雑
Option D: outputFileTracingExcludes による除外(推奨度: ★★☆☆☆)
概要: next.config.tsで memo/ ディレクトリをトレースから除外。
const nextConfig: NextConfig = {
outputFileTracingExcludes: {
"/*": ["./memo/**/*"],
},
};
メリット:
- 設定1行で対応可能
デメリット:
- 公式ドキュメントに「fully static pages are not affected」とあり、SSGページには効果がない可能性
- 根本原因(動的fsスキャン)を解決しない
- Turbopackの警告が消えるかは未検証
- スタンドアロンデプロイ時にメモファイルが含まれない可能性
Option E: メモリストページの分割・最適化(補完的)
概要: /memos リストページのデータ量を削減。
- contentHtmlをリストページで渡さない(タイトル・メタデータのみ)
- サーバーサイドページネーションの追加
- メモ一覧APIルートの作成(クライアントサイドfetch)
メリット: 初期ページサイズの大幅削減 デメリット: 単独ではTurbopack警告を解消しない
5. 推奨アプローチ
Option A(プレビルドインデックス)+ Option C(ハイブリッドSSG)+ Option E(リスト最適化)の組み合わせを推奨。
優先順位:
- Option A: prebuildスクリプトでJSONインデックス生成 → Turbopack警告の根本解消
- Option C: generateStaticParamsで最新N件のみ静的生成 → ビルド時間の大幅短縮
- Option E: リストページの最適化 → ページサイズ改善(次フェーズ可)
段階1だけでもTurbopack警告は完全に解消され、ビルドのコンパイルフェーズが改善される。段階2でSSGページ数を大幅に減らせばビルド全体の時間が改善される。
6. 実装上の注意点
- prebuildスクリプトはCIパイプラインにも組み込む必要がある
- .generated/ ディレクトリは .gitignore に追加(ビルドアーティファクト扱い)
- 開発時は prebuild の自動実行が必要(chokidar watch や next.config.ts の webpack/turbopack plugin)
- JSONインデックスにはcontentHtml(マークダウン変換済み)を含めるか、生マークダウンを含めてランタイムで変換するかの設計判断が必要
- feedルートは最新7日分のみなので、フルインデックスではなく直近データのサブセットで済む可能性もある
参考情報
- Next.js 16 Turbopack ドキュメント: https://nextjs.org/docs/app/api-reference/turbopack
- outputFileTracingExcludes: https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- Turbopack incremental computation: https://nextjs.org/blog/turbopack-incremental-computation
- Turbopack build feedback: https://github.com/vercel/next.js/discussions/77721
- File tracing issue: https://github.com/vercel/next.js/issues/84960