ビルド効率化の分析と改善案
現状のビルドフロー
GitHub Actions ワークフロー(deploy.yml)
1. Checkout(3秒)
2. Install FFmpeg(32秒)
3. Setup Node.js(0秒)
4. Setup pnpm(2秒)
5. Install dependencies(22秒)
6. Generate static site(3分14秒)← ボトルネック
7. Deploy to Cloudflare Pages(17秒)
合計:約4分30秒
Generate static site の内訳
pnpm generate で実行される処理:
- Vite ビルド:TypeScript/Vue のコンパイル
- Nuxt プリレンダリング:全ページの SSR 実行
- OG イメージ生成:nuxt-og-image によるPNG生成
- コンテンツ処理:Markdown/MDX のパース
現状の問題点
1. OG イメージの静的生成(最大の問題)
現状:
nuxt-og-imageが全ページの OG イメージを静的生成crawlLinks: trueにより全リンクをクロール- 生成された OG イメージ:55MB(distの66%)
- ファイル数:359枚のPNG
ログ例:
/__og-image__/static/financial-quiz/jleague/club/urawa/og.png?year=2010 (skipped)
/__og-image__/static/financial-quiz/jleague/club/urawa/og.png?year=2011 (skipped)
...(60クラブ × 複数年度のバリエーション)
(skipped) と表示されていても、クロールと検証で時間がかかる。
2. 毎回フルビルド
- GitHub Actions でキャッシュを使用していない
node_modulesを毎回インストール(22秒)- FFmpeg を毎回インストール(32秒)
3. ローカルとの重複
- ローカルで
pnpm generate済みでも、CI で再度ビルド - 同じ結果を2回生成している
現在の OG イメージ生成方式
パターン1: Cloudflare Workers での動的生成(日本語クイズ)
ファイル: apps/workers/og/src/index.ts
// workers-og ライブラリで動的生成
return new ImageResponse(html, {
width: 1200,
height: 630,
fonts: [{ name: 'Noto Sans JP', data: fontData }]
})
メリット:
- ビルド時間ゼロ
- パラメータに応じた動的な画像
- ストレージ不要
デメリット:
- Workers の実行時間(数十ms)
- フォント読み込みのレイテンシ
パターン2: nuxt-og-image での静的生成(その他全ページ)
設定: nuxt.config.ts
ogImage: {
defaults: {
width: 1200,
height: 630,
renderer: 'satori'
},
fonts: ['Noto+Sans+JP:400', 'Noto+Sans+JP:700'],
runtimeBrowser: false,
compatibility: {
prerender: { satori: 'node', resvg: 'node' }
}
}
メリット:
- アクセス時のレイテンシなし
- CDN でキャッシュされる
デメリット:
- ビルド時間が長い
- dist サイズが大きい(55MB)
- 画像の変更には再ビルド必要
改善案
改善案1: GitHub Actions キャッシュの活用(即効性:中、難易度:低)
actions/setup-node の cache オプションを使うのが最もシンプル:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm' # これだけでpnpmキャッシュが有効に
手動でキャッシュを設定する場合:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
期待効果:Install dependencies を 22秒 → 5秒程度に短縮
改善案2: FFmpeg のキャッシュまたは削除(即効性:高、難易度:低)
質問:FFmpeg は本当に必要か?
- 音声処理(opus変換など)に使用している場合 → ローカルで変換してコミット
- 不要な場合 → 削除で32秒短縮
注意:FFmpeg実行ファイル単体のキャッシュは推奨されない。依存ライブラリが含まれないため動作しない可能性がある。
推奨アプローチ:
- ローカルで音声変換を実行
- 変換済みファイル(.opus/.webm)をGitにコミット
- GitHub ActionsからFFmpegインストールを削除
改善案3: OG イメージのプリレンダリング制限(即効性:高、難易度:中)
オプション A: 特定パターンを除外
// nuxt.config.ts
nitro: {
prerender: {
crawlLinks: true,
ignore: [
/\/jleague\/club\// // Jリーグクラブページを除外
]
}
}
注意:nitro.prerender.ignore はパス部分のみを対象としクエリパラメータ(?year=2010)は含まれない。クエリパラメータ付きURLを制御するにはcrawlLinks の代わりに routes で明示的にプリレンダリング対象を列挙する方が確実。
// より確実な方法
nitro: {
prerender: {
crawlLinks: false, // 自動クロールを無効化
routes: [
'/',
'/about',
// 必要なルートのみ明示的に列挙
]
}
}
オプション B: OG イメージを動的生成に移行
Jリーグクラブページなど、バリエーションが多いページは Cloudflare Workers で動的生成する。
オプション C: ISR(Incremental Static Regeneration)の活用
Nuxt 3 では特定ルートのみオンデマンド生成が可能:
// nuxt.config.ts
routeRules: {
'/financial-quiz/jleague/**': { isr: true } // オンデマンド生成
}
改善案4: ローカルビルド + デプロイのみ(即効性:最高、難易度:低)
考え方:
- ローカルで
pnpm generateを実行 dist/をコミット(または別ブランチにプッシュ)- CI は Cloudflare へのデプロイのみ
メリット:
- CI ビルド時間がほぼゼロ
- ローカルでの確認後にデプロイ
デメリット:
- Git リポジトリが大きくなる(83MB)
- 手動操作が増える
代替案:Cloudflare Pages の Direct Upload API を使用
# ローカルから直接デプロイ
npx wrangler pages deploy dist --project-name=mdx-playground
改善案5: OG イメージ全面 Workers 化(即効性:高、難易度:高)
日本語クイズと同様に、全 OG イメージを Cloudflare Workers で動的生成する。
アーキテクチャ:
/og/{path} → Workers → workers-og で動的生成
メリット:
- ビルド時間大幅短縮(55MB分の生成が不要)
- 柔軟なカスタマイズ
デメリット:
- Workers の実装が必要
- 各コンポーネントの移植
推奨アクション
短期(すぐ実施可能)
- pnpm キャッシュの追加 - 20秒短縮
- FFmpeg の必要性確認 - 不要なら32秒短縮
- Nitro ログレベルを
warnに - ログ削減
中期(数時間の作業)
- OG イメージのプリレンダリング制限 - ignore パターン追加
- Jリーグ OG イメージの Workers 化 - 既存の workers-og を拡張
長期(設計変更)
- ローカルビルド + Direct Upload への移行
- 全 OG イメージの Workers 化
参考情報
現在の dist 構成
| ディレクトリ | サイズ | 説明 |
|---|---|---|
dist/ 全体 | 83MB | 1,090ファイル |
dist/__og-image__/ | 55MB | OGイメージ(359枚) |
| その他 | 28MB | HTML/JS/CSS/コンテンツ |
ビルド時間の内訳(推定)
| 処理 | 時間 | 割合 |
|---|---|---|
| Vite ビルド | 30秒 | 15% |
| プリレンダリング(HTML) | 60秒 | 31% |
| OG イメージ生成 | 100秒 | 52% |
| その他 | 4秒 | 2% |
注意:上記は推定値。(skipped) 表示が多い場合、実際のOGイメージ生成時間はもっと短い可能性がある。正確な内訳を把握するには、DEBUG=nuxt:* や --profile オプションでプロファイリングを取ることを推奨。
FFmpeg 使用状況の詳細調査
使用箇所
nuxt.config.ts の nitro:build:public-assets フック:
// WAV to Opus conversion for audio files in public/audio
execSync(`ffmpeg -y -i "${sourcePath}" -c:a libopus -b:a 96k "${webmPath}"`)
現状の問題
| 項目 | 状況 |
|---|---|
| WAVファイル数 | 6ファイル(boki3/chapter4_0/) |
| Opusファイル数 | 0(Gitにコミットされていない) |
| 変換タイミング | 毎回のビルド時 |
つまり同じWAVファイルを毎回Opusに変換している(無駄)
補足:コマンドでは -c:a libopus でOpusコーデックを使用し、出力は .webm 拡張子(WebMコンテナ)。実際の出力ファイル形式は nuxt.config.ts の実装を確認のこと。
改善方法
- ローカルでOpus(.opus または .webm)に変換
- 変換済みファイルをGitにコミット
- WAVファイルを削除(または.gitignore)
- GitHub ActionsからFFmpegインストールを削除
今後の方針
実施確定
| 項目 | 優先度 | 効果 | 状態 |
|---|---|---|---|
| GitHub Actions pnpmキャッシュ追加 | 高 | 20秒短縮 | ✅ 完了 |
Nitroログレベルを warn に | 中 | ログ削減 | ✅ 完了 |
| WAV→WebM変換をローカル化 | 高 | 32秒短縮(FFmpeg不要に) | ✅ 完了 |
見送り: OGイメージ Workers 化
結論: 検討の結果、現在の静的生成方式を維持することにした。
見送りの理由:
- 現在の構成で十分機能している
- ビルド時間100秒は許容範囲
- 55MBのdistサイズもCDN配信なら問題なし
- OGイメージへのアクセスは実際少ない
- SNSシェア時のクローラーのみがアクセス
- 1日1万アクセス程度のサイトでは影響軽微
- 複雑さを増やすより、シンプルな構成を維持
- Workers化は「やれば速くなる」けど「やらなくても困らない」最適化
- メンテナンスコストを考えると現状維持が妥当
例外: 日本語クイズのOGイメージは動的パラメータ(正解数/問題数)が必要なため、既にWorkers化済み。
詳細: /2025-12-30/og-image-workers-migration.md を参照
関連ファイル
.github/workflows/deploy.yml- GitHub Actions ワークフローapps/web/nuxt.config.ts- Nuxt/Nitro 設定apps/workers/og/src/index.ts- 日本語クイズ OG Workersapps/web/app/components/OgImage/- OG イメージコンポーネントapps/web/public/audio/boki3/- 変換対象のWAVファイル