Next.js App Routerにおけるデータキャッシュと再検証の理解
Grace Collins
Solutions Engineer · Leapcell

はじめに
ウェブ開発の速いペースの世界では、機敏で応答性の高いユーザーエクスペリエンスを提供することが最優先事項です。これを達成するための最も効果的な方法の1つは、インテリジェントなデータキャッシュを介して行われます。しかし、この強力な最適化は予期しない動作を引き起こす可能性もあり、開発者は「なぜ私のfetchリクエストはキャッシュされているのか?」と叫ぶことになります。これは、特にNext.jsのような最新のフレームワーク、特にApp Routerの導入により、一般的な混乱のポイントです。Next.jsがデータキャッシュと再検証をどのように処理するかを理解することは、パフォーマンスが高く予測可能なアプリケーションを構築するために不可欠であり、まさにこの記事で探求することです。Next.jsがデータを管理するために採用している洗練された戦略のレイヤーを剥がし、ユーザーが見るものを常に制御できるようにします。
コアコンセプト
Next.jsのキャッシュメカニズムの複雑さを掘り下げる前に、議論の中心となるいくつかの基本的な用語を定義しましょう。
- キャッシュ: 将来のデータリクエストをより迅速に提供できるように、データまたはファイルのコピーを一時的なストレージ場所に保存するプロセス。
 - 再検証: キャッシュされたデータの鮮度を確認し、必要に応じてキャッシュを更新するために新しいデータを取得するプロセス。
 - 静的レンダリング(SSG - Static Site Generation): ビルド時にページが事前レンダリングされ、CDNから直接提供される静的HTMLファイルが生成されます。これらのページは、新しいビルドまで本質的にキャッシュされます。
 - 動的レンダリング(SSR - Server-Side Rendering): リクエスト時にサーバーでページがレンダリングされます。これにより、動的なデータ取得が可能になりますが、サーバーは内部
fetch呼び出しの結果をキャッシュする場合があります。 - サーバーコンポーネント: React(およびNext.js App Router)の新しいパラダイムであり、コンポーネントをサーバーで直接レンダリングし、クライアントサイドのJavaScriptを削減し、初期ページの読み込みを改善する可能性があります。これらのコンポーネントはデータを直接
fetchできます。 - データキャッシュ: サーバーで行われた
fetchリクエストの結果を格納するためにNext.jsによって使用されるストレージメカニズム(多くの場合、メモリ内またはディスク上)。 - フルルートキャッシュ: データキャッシュを基盤として活用し、レンダリングされたルートの完全なHTMLペイロードを格納するキャッシュ。
 
Next.js App Routerのデータキャッシュと再検証戦略
The Next.js App Routerは、非常に洗練された意見のあるキャッシュシステムを導入しています。その核心は、サーバーコンポーネントおよびその他のサーバーサイドコンテキスト内で行われたデータリクエストをインテリジェントにキャッシュすることです。このキャッシュは単純なキー・バリュー・ストアではなく、レンダリングライフサイクルと深く統合されており、Webのfetch API上に構築されています。
デフォルトでは、サーバーコンポーネント内のfetchリクエストはキャッシュ可能であると見なされます。fetchリクエストがデフォルトのcache: 'force-cache'オプション(指定されていない場合のデフォルト)で実行された場合、Next.jsは応答をデータキャッシュに格納します。後続の同一のfetchリクエスト(同じURL、同じオプション)は、このキャッシュから提供され、データ取得が大幅に高速化されます。
例を挙げて説明しましょう。
// app/page.tsx async function getPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); // 'cache: "force-cache"' オプションは暗黙的です if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); } export default async function Page() { const posts = await getPosts(); return ( <div> <h1>Posts</h1> <ul> {posts.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }
このシナリオでは、getPosts()はAPIからデータを取得します。このルートが最初にレンダリングされるとき、Next.jsは実際のAPI呼び出しを行い、応答をキャッシュします。ユーザーが短時間内にこのページに再度移動した場合、またはサーバーコンポーネントが再レンダリングされた場合(たとえば、異なるユーザーのためだがデータは共有されている)、Next.jsはjsonplaceholder.typicode.comへの別のネットワークリクエストを行うことなく、データキャッシュから直接データを提供します。
キャッシュはいつ無効化されるか、または再検証されるか?
ここでNext.jsの柔軟性が光ります。再検証を管理する方法はいくつかあります。
- 
時間ベースの再検証(
next.revalidate):next.revalidateオプションを使用して、特定のfetchリクエストのキャッシュを指定された時間が経過した後に再検証するようにNext.jsに指示できます。async function getPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts', { next: { revalidate: 60 }, // 60秒ごとに再検証 }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); }revalidate: 60を使用すると、データがキャッシュされてから60秒後、次のリクエストはバックグラウンドでの再検証をトリガーします。古いデータはすぐに提供され、Next.jsは後続のリクエストのためにキャッシュを更新するためにバックグラウンドで新しいデータを静かに取得します。 - 
明示的なキャッシュ無効化(
cache: 'no-store'):fetchリクエストがキャッシュされないようにしたい場合は、明示的にオプトアウトできます。async function getLiveStockPrices() { const res = await fetch('https://api.example.com/stock-prices', { cache: 'no-store', // 常に最新のデータを取得 }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); }これは、頻繁に変更され、古いデータを表示することが許容されない(例:リアルタイム株式相場、ユーザー固有のショッピングカートの内容)非常に動的なデータに最適です。
 - 
ルートセグメントのキャッシュオプトアウト: ルートセグメント全体を動的に設定することができます。これは、そのセグメント内のすべての
fetchリクエストがデフォルトでno-storeになり、ルート自体が動的にレンダリングされることを意味します。これは、layout.tsxまたはpage.tsxファイルからdynamicオプションをエクスポートすることで行います。// app/dashboard/page.tsx または app/dashboard/layout.tsx export const dynamic = 'force-dynamic'; // このセグメントのすべてのfetchが 'no-store' のように動作します'auto'(デフォルト)、'error'、'force-static'などの他のオプションもあります。dynamic = 'force-dynamic'を設定することは、アプリケーション全体で鮮度を確保するための強力な方法です。 - 
オンデマンド再検証(
revalidatePath、revalidateTag): 外部イベント(CMSの更新、eコマース注文など)に基づいてデータが変更される場合、プログラムで再検証をトリガーできます。revalidatePath(path: string): 特定のパスのデータキャッシュを再検証します。revalidateTag(tag: string): 特定の文字列でタグ付けされたすべてのfetchリクエストを再検証します。
revalidateTagを使用するには、fetchリクエストにタグを付ける必要があります。// app/products/page.tsx async function getProducts() { const res = await fetch('https://api.example.com/products', { next: { tags: ['products'] }, // このfetchリクエストにタグを付けます }); if (!res.ok) { throw new Error('Failed to fetch products'); } return res.json(); } // api/revalidate-products.ts (再検証トリガー用APIルート) import { NextRequest, NextResponse } from 'next/server'; import { revalidateTag } from 'next/cache'; export async function GET(request: NextRequest) { const tag = request.nextUrl.searchParams.get('tag'); // 例: ?tag=products if (tag) { revalidateTag(tag); return NextResponse.json({ revalidated: true, now: Date.now() }); } return NextResponse.json({ revalidated: false, message: 'Missing tag' }); }これにより、Webフックまたは管理インターフェイスを構築して、特定のキャッシュエントリをプログラムでクリアし、アプリケーション全体を再構築することなく即座の更新を提供できます。
 
キャッシュ無効化トリガーの理解
Next.jsのデータキャッシュ無効化のトリガーに注意することが重要です。
- デプロイメント: Next.jsアプリケーションの新しいデプロイメントは、すべてのデータキャッシュをクリアします。
 next.revalidateの時間の超過: 前述のように、これはバックグラウンド再検証につながります。- オンデマンド再検証: 
revalidatePathまたはrevalidateTagが明示的に呼び出されます。 - 開発モード: 開発モード(
npm run dev)では、Next.jsは一般的に、常に最新の変更がすぐに表示されるように、より積極的なキャッシュを行いません。本番環境と同じキャッシュ動作を観察できない場合があります。 
結論
Next.js App Routerにおける「なぜ私のfetchはキャッシュされるのか」という難問は、その強力でありながら、時には目に見えない最適化戦略の証です。サーバーコンポーネント内のfetchのデフォルトのキャッシュ動作、およびnext.revalidate、cache: 'no-store'、export const dynamic、オンデマンド再検証関数のような再検証ツールの習得を理解することで、開発者はデータ鮮度を正確に制御し、非常にパフォーマンスの高いユーザーエクスペリエンスを提供できます。Next.jsのデータキャッシュと再検証メカニズムを効果的に活用することは、高速で動的で信頼性の高いWebアプリケーションを構築するための鍵です。