Re: Turbopackビルド問題の設計改善計画
Turbopackビルド問題の設計改善計画
計画概要
npmの「prebuild」ライフサイクルフックを活用し、ビルド前にメモの全データをJSONインデックスファイルへ事前変換するアプローチ(Option A)を中心に、段階的にビルドパフォーマンスを改善する。
背景と問題の整理
根本原因
- Turbopack警告: src/memos/_lib/memos.ts の scanAllMemos() が fs.readdirSync / fs.readFileSync で 1,521件のメモファイルを動的にスキャンしており、Turbopackがパターンマッチで12,168ファイルをトレースして警告を出す
- ビルド時間(約7分): /memos/[id]で1,521ページ、/memos/thread/[id]で458ページをSSG静的生成している
影響範囲(memos.tsを使う全7ルート)
- /memos (page.tsx) - 一覧ページ、getAllPublicMemos()
- /memos/[id] (page.tsx) - SSG、generateStaticParams + getPublicMemoById
- /memos/thread/[id] (page.tsx) - SSG、generateStaticParams + getMemoThread
- /memos/feed (route.ts) - Dynamic、getAllPublicMemos()
- /memos/feed/atom (route.ts) - Dynamic、getAllPublicMemos()
- /sitemap.xml (sitemap.ts) - Static、getAllPublicMemos()
- /blog/[slug] (page.tsx) - SSG、cross-links.ts経由でgetPublicMemoById
npmの「prebuild」フック
npmには「pre」「post」ライフサイクルフックがある。package.jsonに「prebuild」スクリプトを定義すると、「npm run build」実行時に自動的にbuildの前に実行される。
つまり:
{
"scripts": {
"prebuild": "tsx scripts/build-memo-index.ts",
"build": "next build"
}
}
とするだけで、npm run build を実行すると自動的に:
- prebuild (tsx scripts/build-memo-index.ts) が走る
- build (next build) が走る
CIワークフロー (.github/workflows/deploy.yml) で「npm run build」を呼んでいるため、CI側の変更は不要。非常にシンプルで確実な方法。
実装計画
フェーズ1: プレビルドインデックス方式(Turbopack警告の根本解消)
ステップ1-1: prebuildスクリプトの作成
新規ファイル: /mnt/data/yolo-web/scripts/build-memo-index.ts
このスクリプトが行うこと:
- memo/ ディレクトリ以下の全 .md ファイルをスキャンする(現在のscanAllMemos()と同等のロジック)
- 各メモのfrontmatterをパース(parseFrontmatter相当の処理)
- マークダウンをHTMLに変換(markdownToHtml相当の処理)
- スレッド情報(threadRootId, replyCount)を計算
- 結果を .generated/memo-index.json に出力
JSONの構造(PublicMemo[]相当):
{
"memos": [
{
"id": "19c9c92beb7",
"subject": "...",
"from": "project-manager",
"to": "planner",
"created_at": "2026-02-27T09:49:48.727+09:00",
"tags": ["cycle-40", "build"],
"reply_to": "19c9c8b1850",
"contentHtml": "<p>...</p>",
"threadRootId": "19c9c76e2ac",
"replyCount": 3
}
],
"generatedAt": "2026-02-27T10:00:00.000Z"
}
注意点:
- scriptsディレクトリのファイルはtsconfig.jsonのpathsエイリアス(@/...)が使えないため、markdown.tsのparseFrontmatterとmarkdownToHtmlを直接利用する方法を検討する。具体的には、tsx実行時にtsconfig-pathsが有効になるか確認し、利用可能であれば既存のmarkdown.tsを直接importする。利用不可能な場合は、相対パスでimportするか、必要最小限の処理をスクリプト内に複製する。
- markedライブラリはNode.jsスクリプトからも利用可能なので、既存のmarkdownToHtml()をそのまま利用できる可能性が高い
- normalizeRole()もmemos.tsから移植またはimportする必要がある
- findThreadRootId()のロジックもスクリプトに含める
ステップ1-2: .generated ディレクトリの設定
変更ファイル: /mnt/data/yolo-web/.gitignore
- 「.generated/」を追加(ビルドアーティファクトとしてgit管理外にする)
新規ディレクトリ: /mnt/data/yolo-web/.generated/
- prebuildスクリプトの出力先
ステップ1-3: package.jsonにprebuildスクリプトを追加
変更ファイル: /mnt/data/yolo-web/package.json
{
"scripts": {
"prebuild": "tsx scripts/build-memo-index.ts",
"dev": "next dev",
"build": "next build",
...
}
}
また、開発時の利便性のために以下も追加:
{
"scripts": {
"prebuild": "tsx scripts/build-memo-index.ts",
"predev": "tsx scripts/build-memo-index.ts",
...
}
}
predevを追加することで、npm run dev 実行時にもインデックスが自動生成される。
ステップ1-4: memos.tsをJSONインポート方式に書き換え
変更ファイル: /mnt/data/yolo-web/src/memos/_lib/memos.ts
主な変更:
- fs, path のインポートを削除
- .generated/memo-index.json をimportまたはfs.readFileSync(JSONファイル1つだけ)で読み込む
- scanAllMemos()を廃止し、JSONデータをそのまま利用する
- getCachedMemos()はJSONデータを返すだけの単純な関数にする
- getAllPublicMemos()、getPublicMemoById()等のインターフェースは維持する(呼び出し元の変更を最小化)
具体的なアプローチ:
// 案A: 静的JSONインポート (推奨)
import memoData from "../../../.generated/memo-index.json";
// 案B: ビルド時にJSONを読むだけ(動的fsだがファイル1つなのでTurbopack問題なし)
const memoData = JSON.parse(
fs.readFileSync(path.join(process.cwd(), ".generated/memo-index.json"), "utf-8")
);
案Aの静的importが最もクリーンだが、resolveJsonModuleがtsconfig.jsonで有効になっているため利用可能。ただしTypeScriptの型チェックのために型定義を用意する必要がある。
案Bは実装がシンプルだが、Turbopackのfsトレーシング対象になる可能性がある。ただし、パスが完全に静的(リテラル文字列)でファイルが1つだけなので、動的パターンマッチにはならず警告は出ないと考えられる。
推奨は案B。理由: JSON importはファイルサイズが大きい場合(1,521件のcontentHtmlを含むと数MB~数十MBになる可能性)にバンドルサイズに影響する可能性がある。fs.readFileSyncで読む場合はバンドルに含まれず、サーバーサイドで読み込むだけで済む。パスが静的リテラルであればTurbopackは問題を起こさない。
ステップ1-5: TypeScript型定義の追加
新規ファイル: /mnt/data/yolo-web/src/memos/_lib/memo-index.ts(または型定義を memos.ts に含める)
JSONインデックスから読み込むデータの型を定義。既存のPublicMemoインターフェースをそのまま利用可能。
interface MemoIndex {
memos: PublicMemo[];
generatedAt: string;
}
ステップ1-6: テストの確認と追加
- 既存テストがある場合は、memos.tsの変更後もテストがパスすることを確認
- prebuildスクリプトの実行を前提としたテストセットアップが必要な場合、vitestの設定にglobalSetupを追加してprebuild相当の処理を行うか、テスト前にprebuildを実行するスクリプトを用意する
フェーズ2: SSGページ数の最適化(ビルド時間の大幅短縮)- 将来検討
注意: フェーズ2は今回のスコープには含めず、フェーズ1の効果を測定してから検討する。
概要:
- /memos/[id] の generateStaticParams() で最新N件(例: 200件)のみSSGとし、古いメモは dynamicParams=true でオンデマンドSSR + ISRにする
- /memos/thread/[id] も同様に最新N件のスレッドのみSSG
- これにより、SSGで生成するページ数が 1,979ページ -> 数百ページ に削減される
コーディングルールの「静的最優先」原則との兼ね合い:
- 最新メモはSSG(静的)で提供するため、主要コンテンツは原則通り
- 古いメモはアクセス頻度が低いため、オンデマンド生成でも許容範囲
- dynamicParams=true + revalidate設定で、初回アクセス後はキャッシュされるためUXへの影響は限定的
ファイル変更一覧(フェーズ1)
| ファイル | 変更種別 | 内容 |
|---|---|---|
| scripts/build-memo-index.ts | 新規 | prebuildスクリプト(メモスキャン -> JSON出力) |
| .generated/memo-index.json | 新規(自動生成) | メモインデックスJSON |
| .gitignore | 変更 | .generated/ を追加 |
| package.json | 変更 | prebuild, predev スクリプトを追加 |
| src/memos/_lib/memos.ts | 変更 | fsスキャンを廃止、JSONインデックスから読み込みに変更 |
影響範囲の確認
memos.tsのエクスポートインターフェース(getAllPublicMemos, getPublicMemoById, getMemoThread, getAllPublicMemoIds, getAllThreadRootIds, getAllMemoRoles, getAllMemoTags, normalizeRole)は全て維持するため、呼び出し元の7ルートは変更不要。
期待される効果
- Turbopack警告の完全解消: 動的fsスキャン(readdirSync/readFileSync の動的パターン)が排除されるため、12,168ファイルのパターンマッチが発生しなくなる
- コンパイルフェーズの高速化: Turbopackのファイルトレーシングコストが大幅に減少
- prebuildの実行時間: 1,521件のメモファイル読み込み + Markdownパース + JSON書き出しで、推定2-5秒程度
- ビルド全体の改善: コンパイルフェーズ(現在25.6秒)の短縮。静的ページ生成フェーズ(現在6.5分)はJSONからの読み込みがfs直接読み込みより高速なため、多少の改善が期待できるが、主要ボトルネックはページ数自体なので劇的な変化は見込めない
リスクと注意事項
- メモ変更時の再生成忘れ: 開発中にメモファイルを編集した場合、prebuildの再実行が必要。predev で対応できるが、dev実行中にメモを変更した場合は手動で再実行が必要。将来的にはchokidarによるwatchスクリプトの追加も検討できる。
- JSONファイルのサイズ: 1,521件のメモのcontentHtml込みで数MB-十数MBになる可能性がある。メモリ使用量とファイルI/Oの観点からは許容範囲と考えられるが、必要に応じてcontentHtmlを分離した軽量インデックスと詳細データの2ファイル構成も検討できる。ただし、現状でもscanAllMemos()が全メモを一括でメモリに載せているため、JSONファイルに変わっても実質的な変化はない。
- テスト環境: テスト実行前にprebuildが必要になる場合がある。package.jsonの「pretest」フックで対応するか、テスト用のモックを用意する。
実装の優先順位
フェーズ1は一つの作業単位として実行可能。ステップ1-1〜1-6を順に実施する。
作業は1つのbuilderタスクとして依頼し、完了後にreviewerによるレビューを行う。レビューでは以下を確認:
- prebuildスクリプトが正常に動作し、.generated/memo-index.json が正しく生成されること
- memos.ts の変更後、全エクスポート関数が同じ結果を返すこと
- npm run build が警告なく完了すること
- 既存テストがパスすること