Next.js 16のCache ComponentsとSupabaseクエリキャッシュの実装によるパフォーマンス改善前後の比較イラスト。ブラウザ、Next.jsサーバー(キャッシュ含む)、Supabase APIのデータフローと、成功を喜ぶキャラクター、CI/CDビルド安定化のアイコンが描かれたアイキャッチ画像。

Next.js 16の新機能「Cache Components」を使って、Supabaseからのデータ取得をサーバーサイドでキャッシュできるようにしました。

クライアントサイドで毎回フェッチしていた数百件規模のマスターデータを、サーバーサイドキャッシュに移行することで、初回表示を高速化できました。

導入自体はシンプルですが、CI環境でのビルドエラーにハマった経験も踏まえて、実装のポイントを共有します。

こんな人におすすめ

この記事は以下のような方に参考になります。

  • Next.js 16のCache Components機能を試してみたい方
  • Supabaseから取得するデータをサーバーサイドでキャッシュしたい方
  • CI/CD環境でのビルドエラーに悩んでいる方
  • クライアントサイドフェッチからサーバーサイドキャッシュへの移行を検討している方

目次

背景

業務用の検索アプリで、Supabaseから取得するマスターデータ(数百件規模)のパフォーマンスを改善するため、Next.js 16のCache Components機能を導入しました。

従来はクライアントサイドで毎回フェッチしていましたが、サーバーサイドキャッシュに移行することで初回表示を高速化しています。

要件

  • Supabaseから取得するマスターデータをサーバーサイドでキャッシュする
  • キャッシュライフサイクル:30分ステール、1時間再検証、1日有効期限
  • 別データソースへのフォールバックとの互換性を維持
  • CI環境(ダミーSupabase URL)でのビルドエラーを回避する

キャッシュライフサイクルのイメージ

Next.js 16のキャッシュは、stale(新鮮)→ revalidate(再検証)→ expire(期限切れ)の3段階で管理されます。

flowchart TD
    A[リクエスト] --> B{キャッシュ存在?}

    B -->|なし| C[Supabaseから取得]
    C --> D[キャッシュ保存]
    D --> E[レスポンス返却]

    B -->|あり| F{stale期限内?<br/>30分以内}
    F -->|はい| G[キャッシュを即座に返却]

    F -->|いいえ| H{revalidate期限内?<br/>1時間以内}
    H -->|はい| I[キャッシュを返却]
    I --> J[バックグラウンドで再取得]

    H -->|いいえ| K{expire期限内?<br/>1日以内}
    K -->|はい| L[再取得を待って返却]

    K -->|いいえ| C

実装方針

1. serverCache.ts - キャッシュ関数の実装

import { cacheTag, cacheLife } from "next/cache";

export async function getCachedItems(): Promise<Item[]> {
  "use cache";
  cacheTag("items");
  cacheLife("items");

  return await fetchItemsFromSupabase();
}

2. next.config.ts - キャッシュライフサイクル設定

cacheLife: {
  items: {
    stale: 1800,      // 30分
    revalidate: 3600, // 1時間
    expire: 86400,    // 1日
  },
},

3. app/page.tsx - Server Component統合

export default async function Home() {
  let initialItems: Item[] | null = null;

  if (isSupabaseEnabled() && isValidSupabaseUrl()) {
    try {
      initialItems = await getCachedItems();
    } catch (error) {
      initialItems = null; // フォールバック
    }
  }

  return <App initialItems={initialItems} />;
}

データ取得フローの全体像

サーバーサイドキャッシュを導入したことで、データの取得フローが以下のようになりました。

sequenceDiagram
    participant Browser as ブラウザ
    participant Server as Next.js Server
    participant Cache as Next.js Cache
    participant Supabase as Supabase API

    Browser->>Server: ページリクエスト
    Server->>Cache: キャッシュ確認

    alt キャッシュヒット(stale期限内)
        Cache-->>Server: キャッシュデータ返却
        Server-->>Browser: HTML + 初期データ
    else キャッシュミス または 期限切れ
        Server->>Supabase: データ取得リクエスト
        Supabase-->>Server: マスターデータ(数百件)
        Server->>Cache: キャッシュ保存
        Server-->>Browser: HTML + 初期データ
    end

    Note over Browser,Cache: 初回表示完了後、クライアントサイドでの<br/>追加フェッチは不要

4. useItemSearch.ts - 初期データ受け取り

if (initialItems && initialItems.length > 0) {
  // サーバーキャッシュからのデータを使用
  actions.initSuccess(initialItems);
  return initialItems;
}
// 従来のクライアントサイドフェッチにフォールバック

つまづいた点

CI環境でのビルドエラー

ここで困ったのが、CI環境でのビルドエラーです。

CIではダミーのSupabase URL(example.supabase.co)が設定されているため、ビルド時の静的生成でfetchが失敗してしまいます。

解決策: ダミーURLを検出してサーバーサイドキャッシュをスキップ

function isValidSupabaseUrl(): boolean {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
  return url.length > 0 && !url.includes("example.supabase.co");
}

環境判定とフォールバックのフロー

flowchart TD
    A[ページリクエスト] --> B{Supabase有効?}
    B -->|無効| C[initialItems = null]

    B -->|有効| D{有効なURL?}
    D -->|ダミーURL| C

    D -->|本番URL| E[getCachedItems呼び出し]

    E --> F{キャッシュ取得成功?}
    F -->|成功| G[initialItems = キャッシュデータ]

    F -->|失敗| H{別データソース有効?}
    H -->|有効| I[別データソースから取得]
    H -->|無効| C
    I --> J[initialItems = フォールバックデータ]

    G --> K[App コンポーネント描画]
    J --> K
    C --> K

    subgraph "&nbsp;useItemSearch.ts&nbsp;"
        K --> L{initialItemsあり?}
        L -->|あり| M[キャッシュデータを使用]
        L -->|なし| N[クライアントサイドフェッチ]
    end

force-dynamic との互換性

cacheComponents: trueexport const dynamic = 'force-dynamic' は互換性がありません。

Next.js 16のCache Components機能を使う場合は、別の方法でビルド時エラーを回避する必要があります。

型変換の注意点

  • supabaseService.getItems()SearchResult[] を返します
  • アプリは Item[](レガシー型)を使用しています
  • supabaseAdapter.fetchItems() で変換したデータを取得しています

良かった点

実装して一番良かったのは、コードのシンプルさです。

"use cache" ディレクティブと cacheTagcacheLife を追加するだけで、複雑なキャッシュロジックを自前で実装する必要がありませんでした。

また、フォールバック設計を残しておいたことで、Supabaseに問題が発生しても別のデータソースに自動的に切り替わる安心感があります。

得られた効果

キャッシュ導入後、以下の効果を確認できました。

パフォーマンス改善の内訳

pie showData
    title 改善効果の内訳
    "初回表示高速化" : 40
    "サーバー負荷軽減" : 30
    "CI/CD安定化" : 20
    "コード保守性向上" : 10

具体的な改善ポイント

  • 初回表示の高速化: 数百件規模のデータをサーバーサイドでキャッシュすることで、クライアントサイドのフェッチ待ち時間が削減されました
  • サーバー負荷の軽減: 同じデータへの重複リクエストが減り、Supabaseへのアクセス頻度が低下しました
  • CI/CDの安定化: ダミーURL検出により、CI環境でのビルドが確実に成功するようになりました

改善前後の比較

flowchart LR
    subgraph "&nbsp;改善前&nbsp;"
        A1[ブラウザ] --> A2[クライアントJS]
        A2 --> A3[Supabase API]
        A3 --> A4[数百件取得]
        A4 --> A2
        A2 --> A1
    end

    subgraph "&nbsp;改善後&nbsp;"
        B1[ブラウザ] --> B2[Next.js Server]
        B2 --> B3{キャッシュ?}
        B3 -->|あり| B4[即座に返却]
        B3 -->|なし| B5[Supabase API]
        B5 --> B6[取得+キャッシュ保存]
        B6 --> B4
        B4 --> B1
    end

    style A3 fill:#ffcccc
    style B5 fill:#ccffcc

まとめ

Next.js 16のCache Componentsは、サーバーサイドキャッシュを非常にシンプルに実装できる強力な機能です。

ただし、CI/CD環境での考慮や force-dynamic との互換性など、いくつかの注意点もあります。

4つのポイント

  1. Next.js 16のCache Componentsは強力だが制約がある

    • use cache ディレクティブでシンプルにキャッシュを実装
    • ただし force-dynamic との併用はできない
  2. CI/CD環境での考慮が必須

    • ダミー環境変数でのビルドは静的生成時に問題を起こす
    • 環境検出によるスキップ処理が必要
  3. フォールバック設計の重要性

    • サーバーサイドキャッシュ失敗時はクライアントサイドフェッチで対応
    • データソース切り替え(Supabase/フォールバック先)との互換性を維持
  4. テストカバレッジの確保

    • initialItems オプションの全パターンをテスト
    • next/cache はJest環境でモック必須