ビルド効率化の分析と改善案

現状のビルドフロー

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 で実行される処理:

  1. Vite ビルド:TypeScript/Vue のコンパイル
  2. Nuxt プリレンダリング:全ページの SSR 実行
  3. OG イメージ生成:nuxt-og-image によるPNG生成
  4. コンテンツ処理: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-nodecache オプションを使うのが最もシンプル:

- 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実行ファイル単体のキャッシュは推奨されない。依存ライブラリが含まれないため動作しない可能性がある。

推奨アプローチ

  1. ローカルで音声変換を実行
  2. 変換済みファイル(.opus/.webm)をGitにコミット
  3. 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 の実装が必要
  • 各コンポーネントの移植

推奨アクション

短期(すぐ実施可能)

  1. pnpm キャッシュの追加 - 20秒短縮
  2. FFmpeg の必要性確認 - 不要なら32秒短縮
  3. Nitro ログレベルを warn - ログ削減

中期(数時間の作業)

  1. OG イメージのプリレンダリング制限 - ignore パターン追加
  2. Jリーグ OG イメージの Workers 化 - 既存の workers-og を拡張

長期(設計変更)

  1. ローカルビルド + Direct Upload への移行
  2. 全 OG イメージの Workers 化

参考情報

現在の dist 構成

ディレクトリサイズ説明
dist/ 全体83MB1,090ファイル
dist/__og-image__/55MBOGイメージ(359枚)
その他28MBHTML/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 の実装を確認のこと。

改善方法

  1. ローカルでOpus(.opus または .webm)に変換
  2. 変換済みファイルをGitにコミット
  3. WAVファイルを削除(または.gitignore)
  4. GitHub ActionsからFFmpegインストールを削除

今後の方針

実施確定

項目優先度効果状態
GitHub Actions pnpmキャッシュ追加20秒短縮✅ 完了
Nitroログレベルを warnログ削減✅ 完了
WAV→WebM変換をローカル化32秒短縮(FFmpeg不要に)✅ 完了

見送り: OGイメージ Workers 化

結論: 検討の結果、現在の静的生成方式を維持することにした。

見送りの理由:

  1. 現在の構成で十分機能している
    • ビルド時間100秒は許容範囲
    • 55MBのdistサイズもCDN配信なら問題なし
  2. OGイメージへのアクセスは実際少ない
    • SNSシェア時のクローラーのみがアクセス
    • 1日1万アクセス程度のサイトでは影響軽微
  3. 複雑さを増やすより、シンプルな構成を維持
    • 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 Workers
  • apps/web/app/components/OgImage/ - OG イメージコンポーネント
  • apps/web/public/audio/boki3/ - 変換対象のWAVファイル