cache,revalidatePath,revalidateTagについて(Next.js15)
はじめに
私はNext.jsを使用するにあたりキャッシュの勉強を後回しにしていました。
また、普段見ているYouTubeにrevalidatePathという単語が出てくることがあり、気になっていました。
本記事はキャッシュについて、データキャッシュの再検証(revalidatePath,revalidateTag)関して公式ドキュメントをもとに学習し、それについてまとめた記事となります。
キャッシュについて
Next.jsには4種類のキャッシュが存在します。
- リクエストのメモ化
- フルルートキャッシュ
- クライアント側ルーターキャッシュ
- データキャッシュ
リクエストのメモ化
Next.jsは同じURLへのリクエストを自動的にメモ化してくれます。これにより複数の場所で同じデータを取得するfetch関数を用いても、実行されるのは1回のみとなります。

フルルートキャッシュ
Next.jsは最適化の為に、ビルド時にルートを自動的にレンダリングしキャッシュします。リクエストごとにサーバー上でレンダリングするのではなく、キャッシュされたルートを提供してくれるため、ページの読み込みが高速化されます。これは静的ルートの場合のみ行われます。つまり、SSGとISRで生成されたページに関してはサーバー側でキャッシュされます。
動的ルート、つまりSSRの場合はフルルートキャッシュの参照はスキップされ、データキャッシュの参照やfetchの処理に移ります。その際に、Reactサーバーコンポーネントペイロードをクライアントのルーターキャッシュに保存します。以降そのページのプリフェッチを行う際にはペイロードが保存されているかを確認し、保存されている場合はサーバーへの新しいリクエストの送信をスキップします。

クライアント側ルーターキャッシュ
Next.jsにはレイアウト、読み込み状態、ページそれぞれのReactサーバーコンポーネントペイロードを保存するクライアント側ルーターキャッシュというものがあります。
ユーザーがルート間を移動した際、訪問済みのルートセグメントをキャッシュし、ユーザーが移動する可能性が高いルートをプリフェッチします。これにより即時に「戻る/進む」というナビゲーションを行うことが可能になります。
データキャッシュ
一度fetchしたデータを保持することができます。これによりオリジンデータソースへのリクエストを減らすことができます。
fetch(url, {cache: 'force-cache'})
という形でリクエストされた場合、最初にデータキャッシュを確認しに行きます。キャッシュがない場合はオリジンデータソースにデータを取りに行き、データを保持します。キャッシュがある場合はキャッシュを返します。
fetch(url, {cache: 'no-store'})
という形でリクエストされた場合、データキャッシュの確認をスキップしてオリジンデータソースにデータを取りに行きます。

キャッシュされたデータを再検証する方法は2種類あります。
- 時間ベースの再検証
- オンデマンド再検証
時間ベースの再検証
fetch(url, {next: {revalidate: 60 }})
という形で初めてリクエストされた場合、オリジンデータソースにデータを取りに行き、データはデータキャッシュに保存されます。
指定された時間(60s)内のリクエストに対してはキャッシュを返します。
指定された時間(60s)を過ぎた際のリクエストを受け付けた際に再検証が行われます。しかし、再検証が完了するまではキャッシュが返されます。
再検証完了後は再検証により新たに保存されたキャッシュが返されます。

オンデマンド再検証
オンデマンド再検証を行うには2種類の方法があります。
- revalidatePath
- revalidateTag
イベントがトリガーとなりrevalidatePathは指定されたパス、revalidateTagは指定されたタグに関連するデータを再検証します。
revalidatePath('/')
fetch(url, {cache:'force-cache', next: { tags: ["example"] } })
...
...
revalidateTag('example')
データキャッシュのオンデマンド再検証を行ってみる
先ほどデータキャッシュの項にでてきたrevalidatePathとrevalidateTagについて、実際にアプリを作成し触っていきます。
アプリ概要

AクラスとBクラスの生徒情報が表示されており、
「すべて更新する」ボタンを押すと↓が発火し、画面全体の情報を最新の状態に更新する。
revalidatePath('/')
「Aクラスのみ更新する」ボタンを押すと↓が発火し、Aクラスのみ情報を最新の状態に更新する。
revalidateTag('classA')
「Bクラスのみ更新する」ボタンを押すと↓が発火し、Bクラスのみ情報を最新の状態に更新する。
revalidateTag('classB')
環境
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.3"
APIにはmockAPIというサービスを使用しています。
ディレクトリ構造
.
└── app
├── components
│ ├── AllUpdateButton.tsx
│ ├── ClassA.tsx
│ ├── ClassB.tsx
│ └── Title.tsx
├── lib
│ └── actions.ts
├── favicon.ico
├── globals.css
├── layout.tsx
├── page.tsx
└── type.ts
コード
import { revalidateClassA } from "../lib/actions";
import { StudentDataType } from "../type";
async function getClassAData() {
const url =
"https://68c651b5442c663bd026b6c1.mockapi.io/api/classroom/classA";
const response = await fetch(url, {cache:'force-cache', next: { tags: ["classA"] } });
return response.json();
}
const ClassA = async () => {
const classA = await getClassAData();
return (
<div className="py-3 mb-20">
<form
action={revalidateClassA}
className="flex items-center justify-between mb-10"
>
<h2 className="text-xl">Aクラス</h2>
<button className="cursor-pointer bg-gray-700 text-white px-5 py-2 rounded-xl hover:scale-101 transition delay-50">
Aクラスのみ更新する
</button>
</form>
<div className="grid grid-flow-row grid-cols-5 gap-5">
{classA.map((student: StudentDataType) => (
<div
key={student.id}
className="border-1 border-gray-300 py-2 rounded-md text-center inset-shadow-sm"
>
<h3>{student.name}</h3>
<p>age : {student.age}</p>
</div>
))}
</div>
</div>
);
};
export default ClassA;
import { revalidateClassB } from "../lib/actions";
import { StudentDataType } from "../type";
async function getClassBData() {
const url =
"https://68c651b5442c663bd026b6c1.mockapi.io/api/classroom/classB";
const response = await fetch(url, {cache:'force-cache', next: { tags: ["classB"] } });
return response.json();
}
const ClassB = async () => {
const classB = await getClassBData();
return (
<div className="py-3">
<form
action={revalidateClassB}
className="flex items-center justify-between mb-10"
>
<h2 className="text-xl">Bクラス</h2>
<button className="cursor-pointer bg-gray-700 text-white px-5 py-2 rounded-xl hover:scale-101 transition delay-50">
Bクラスのみ更新する
</button>
</form>
<div className="grid grid-flow-row grid-cols-5 gap-5">
{classB.map((student: StudentDataType) => (
<div
key={student.id}
className="border-1 border-gray-300 py-2 rounded-md text-center inset-shadow-sm"
>
<h3>{student.name}</h3>
<p>age : {student.age}</p>
</div>
))}
</div>
</div>
);
};
export default ClassB;
import { revalidateAll } from "../lib/actions";
const AllUpdateButton = () => {
return (
<form action={revalidateAll}>
<button className="block m-auto cursor-pointer bg-gray-700 text-white px-5 py-2 rounded-xl mb-10 hover:scale-101 transition delay-50">
すべて更新する
</button>
</form>
);
};
export default AllUpdateButton;
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function revalidateAll() {
revalidatePath('/')
}
export async function revalidateClassA() {
revalidateTag('classA')
}
export async function revalidateClassB() {
revalidateTag('classB')
}
動作確認
classAとclassBともにデータ数が5個からともに10個に増やし、「すべて更新する」ボタンを押すとページ全体が再検証できていることが確認できる。

↓


↓

classAのデータ数を5個から10個に増やし、classBのデータ数を5個から100個に増やし、「Aクラスのみ更新する」ボタンを押すとAクラスの情報のみが再検証できていることが確認できる。

↓


↓

classBのデータ数を5個から10個に増やし、classAのデータ数を5個から100個に増やし、「Bクラスのみ更新する」ボタンを押すとBクラスの情報のみが再検証できていることが確認できる。

↓


↓

おわりに
今回はNext.jsにあけるキャッシュとデータキャッシュの再検証方法としてrevalidatePathとrevalidateTagについて学びました。
いままでキャッシュについて意識したことがありませんでしたが、キャッシュの仕組みを理解することで今後キャッシュ戦略が必要となった際に生きてくると思います。
参考
Discussion