revalidateがCloudflare Pagesで動かない理由

revalidateがCloudflare Pagesで動かない理由

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。

Next.jsのApp RouterでISRを設定したのに、Cloudflare Pagesにデプロイしたら古いデータが表示され続ける——そういう現象に直面している方に向けて書きます。この記事では、revalidateが動かない根本的な理由を仕組みから解説し、現実的な代替手段まで整理します。

revalidate の役割をおさらいする

App Routerでは、ルートセグメントに次のように書くことでISR(Incremental Static Regeneration)が有効になります。

// app/products/page.tsx
export const revalidate = 60;

「最初のリクエストで生成したHTMLをキャッシュし、60秒が経過した後に次のリクエストが来たとき、バックグラウンドで再レンダリングして新しいキャッシュを作る」という動作です。合わせて revalidatePath() や revalidateTag() を呼ぶことで、オンデマンドにキャッシュを破棄することもできます。

'use server';
import { revalidatePath } from 'next/cache';

export async function updateProduct(id: string) {
  // DB更新処理...
  revalidatePath('/products');
}

公式ドキュメントによると、これらの仕組みはサーバー上の .next/cache ディレクトリにレンダリング済みのHTMLやフェッチデータを書き込み、次のリクエスト時にそれを参照することで成立しています。

Cloudflare Pages のランタイムは Node.js ではない

Cloudflare Pagesは裏側でCloudflare Workers(V8エンジン)を使ってNext.jsアプリを動かします。これはNode.jsとは根本的に異なるランタイムです。

項目 Node.js環境(セルフホスト) Cloudflare Workers
ファイルシステム 書き込み可能な永続ストレージ 読み取り専用(ビルド成果物のみ)
プロセスの寿命 常駐する単一プロセス リクエストごとに起動・破棄
キャッシュ保存先 .next/cache(ローカルディスク) 保存先が存在しない
インスタンス間の状態共有 同一プロセス内で共有 原則として共有不可

Workerはステートレスです。あるWorkerインスタンスが生成したキャッシュは、次のリクエストを処理する別のインスタンスには引き継がれません。

revalidate が動かない根本原因

Next.jsのrevalidateは「永続的なキャッシュストレージがサーバーに存在する」ことを前提として設計されています。.next/cache/fetch-cache や .next/server/app 以下にデータを書き込み、次のリクエストでそれを読み返す仕組みです。Cloudflare Workers環境ではこの前提が成り立ちません。

時間ベースISR(export const revalidate = N)の場合

ビルド時に生成した静的HTMLは配信できます。しかし「N秒後に再レンダリングしてキャッシュを更新する」動作が機能しません。Workerがリクエスト処理後に終了すると、「いつキャッシュが生成されたか」という情報自体が消えてしまいます。

オンデマンドrevalidation(revalidatePath() / revalidateTag())の場合

これらの関数はサーバーのキャッシュストアに対してフラグを立てる操作です。書き込み先のストレージがない環境では、エラーにならずに呼び出しが無視されるか、その後のリクエストが期待通りに再フェッチされないと考えられます。

@cloudflare/next-on-pages アダプターを使った場合も、この制約は変わりません(執筆時点)。アダプターはビルド成果物をWorkers向けに変換しますが、Workersのステートレスな実行モデル自体は変わらないためです。

Next.jsとCloudflare Pagesの相性問題はrevalidateだけではありません。middlewareのcookie処理にも同様の制約があります。詳しくはNext.js App Router middleware の cookie が Cloudflare Pages で動かない原因も参考になります。

症状を確認する手順

「revalidateが効いていないかもしれない」と疑ったら、まずルートセグメントに次を追加します。

export const dynamic = 'force-dynamic';

これでキャッシュを完全に無効化し、毎回サーバーサイドでレンダリングさせます。表示内容が変わるならキャッシュが原因です。変わらないなら、データ取得ロジックやDB接続に問題がある可能性があります。

Cloudflare PagesのFunctions logも確認してください。revalidatePath() の呼び出しはエラーを出さずに無視されることがあるため、ログに何も残らないケースがあります。また、Cloudflareのエッジキャッシュ(CDN層)とNext.jsのサーバーキャッシュは別物です。CDNキャッシュが原因なら、Cloudflareダッシュボードの「Purge Cache」で切り分けられます。

代替手段

force-dynamic でキャッシュを無効化する

最も単純な解決策です。データの鮮度が重要でキャッシュが不要なルートに適用します。

export const dynamic = 'force-dynamic';

ページがSSR(毎回サーバーレンダリング)になるため、Functionsの実行時間・回数が増加します。Cloudflare Pagesの無料枠にはFunctions実行回数の上限があります(執筆時点の公式ドキュメントで確認してください)。アクセス量と相談した上で採用してください。

Cloudflare KV を使った手動キャッシュ

KVはWorkers間で共有できるKey-Valueストアです。時間ベースのrevalidateと同等の動作を実装できます。

// app/api/products/route.ts
import { getRequestContext } from '@cloudflare/next-on-pages';

export const runtime = 'edge';

export async function GET() {
  const { env } = getRequestContext();
  const cacheKey = 'products:all';
  const cached = await env.MY_KV.get(cacheKey, 'json');
  if (cached) return Response.json(cached);

  const data = await fetchProducts();
  await env.MY_KV.put(cacheKey, JSON.stringify(data), {
    expirationTtl: 60,
  });
  return Response.json(data);
}

オンデマンドのキャッシュ無効化は env.MY_KV.delete(cacheKey) で行います。商品更新のServer ActionでKVのキーを削除すれば、revalidatePath() と同等の動作を自前で実装できます。

Node.js 環境に切り替える

私のECサイトはNode.jsで常駐させて自宅サーバー+Cloudflare Tunnelで公開する構成を採用しています。Node.jsランタイムそのままなので、Next.jsのrevalidateは設計通りに動作します。Workersの制約を根本的に避けたい場合、VPSやセルフホストも選択肢になります。Prismaのようなネイティブモジュールを使うプロジェクトはCloudflare Workers環境との相性に別の問題もあるため(環境によって異なります)、Node.js環境に戻すのが最もシンプルな解決策になる場合があります。

まとめ

状況 動かない理由
export const revalidate = N Workerがステートレスでキャッシュ生成日時が保持されない
revalidatePath() 書き込み先のキャッシュストアがWorker環境に存在しない
revalidateTag() 同上

Next.jsのrevalidateがCloudflare Pagesで動かない根本原因は、「Node.jsの永続サーバーを前提としたキャッシュ設計」とWorkersのステートレスな実行モデルの不一致です。

まず export const dynamic = 'force-dynamic' を追加して原因を切り分けてください。それで問題が再現するなら、データの鮮度要件とコスト・構成の制約を照らし合わせ、KVによる手動キャッシュかNode.js環境への移行を検討するのが現実的な進め方です。

この記事で触れたもの

よくある質問

revalidatePath()を呼び出してもエラーにならないのになぜキャッシュが更新されないのですか?

Cloudflare Workers環境にはNext.jsのキャッシュストアが存在しないため、関数が呼ばれても書き込み先がなく、エラーを出さず静かに無視されます。動作がサイレントに失敗するため原因の特定が難しいです。

@cloudflare/next-on-pagesアダプターを使えばrevalidateは動きますか?

執筆時点では動きません。アダプターはビルド成果物をWorkers向けに変換しますが、Workersのステートレスな実行モデル自体は変わらないため、永続的なキャッシュストレージが存在しないという制約は残ります。

Cloudflare Pagesで時間ベースのISRを実現するにはどうすればいいですか?

Cloudflare KVにexpirationTtlを設定して手動でキャッシュを実装するのが現実的です。KVはWorkerインスタンス間で共有されるため、時間ベースのrevalidateの代替として機能します。