Next.js の App Router では、Server Component の中で fetch を呼ぶだけでデータを取得できますが、その結果がキャッシュされるのか、毎回取り直すのかは指定の仕方で変わります。うまく使えば、ページを高速に保ちながら一定間隔でデータを更新する ISR(Incremental Static Regeneration)を簡単に実現できます。この記事では、fetch のキャッシュ挙動の基本から、revalidate による時間ベースの再検証、cache: "no-store" による動的な取得、タグを使ったオンデマンド再検証、そしてルートセグメント設定までを解説します。あわせて、Next.js 14 と 15 で既定のキャッシュ挙動が変わった重要な違いにも触れます。
目次
Server Component の fetch とキャッシュの基本
App Router の Server Component では、ブラウザの fetch と同じ書き方でデータを取得できます。次のコードは、外部 API から記事一覧を取得して表示する最小の例です。
type Post = { id: number; title: string };
export default async function Page() {
// Server Component なので async 関数にして await できる
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ポイントは、Next.js が fetch を拡張していることです。標準の fetch に cache と next という2つのオプションが追加されており、これらを渡すことで取得結果をキャッシュするか、どのくらいの間隔で取り直すかを制御できます。オプションを何も指定しなかったときの挙動は Next.js のバージョンによって異なるため、この記事の後半で詳しく説明します。まずは、明示的に挙動を指定する書き方から見ていきます。
指定できる主なオプションは次のとおりです。
| オプション | 挙動 |
|---|---|
{ cache: "force-cache" } | 結果をキャッシュし、再検証されるまで使い回す(静的) |
{ cache: "no-store" } | キャッシュせず、リクエストのたびに取得する(動的) |
{ next: { revalidate: 60 } } | キャッシュしつつ、指定秒数ごとに再検証する(ISR) |
{ next: { tags: ["posts"] } } | キャッシュにタグを付け、オンデマンドで再検証できるようにする |
revalidate で時間ベースに再取得する(ISR)
ニュースや商品一覧のように「頻繁ではないが定期的に更新したい」データには、時間ベースの再検証が向いています。next.revalidate に秒数を渡すと、その秒数のあいだはキャッシュした結果を返し、経過後の最初のアクセスをきっかけにバックグラウンドで再取得します。これが Next.js における ISR の仕組みです。
export default async function PostsPage() {
const res = await fetch("https://api.example.com/posts", {
// 60 秒間はキャッシュを使い、経過後に再検証する
next: { revalidate: 60 },
});
const posts = await res.json();
return <pre>{JSON.stringify(posts, null, 2)}</pre>;
}
この例では、60 秒以内のアクセスはすべて同じキャッシュ結果を返すため高速です。60 秒を過ぎてからアクセスがあると、いったんは古いキャッシュを返しつつ裏で新しいデータを取得し、次回以降のアクセスに反映します。「常に最新でなくてよいが、放置しすぎもしたくない」という要件にちょうど合う方式です。revalidate を指定した fetch は自動的にキャッシュ対象になるので、cache: "force-cache" を別途書く必要はありません。
no-store で毎回最新を取得する(動的)
ログイン中のユーザー情報やカートの中身のように、リクエストごとに必ず最新であってほしいデータには cache: "no-store" を使います。これを指定した fetch はキャッシュされず、ページが表示されるたびに API へ問い合わせます。
export default async function DashboardPage() {
const res = await fetch("https://api.example.com/me", {
// キャッシュせず、毎回取得する
cache: "no-store",
});
const user = await res.json();
return <p>こんにちは、{user.name} さん</p>;
}
キャッシュしない fetch を含むページは動的レンダリングになり、ビルド時ではなくリクエストのたびにサーバーで生成されます。常に最新を返せる反面、毎回 API を呼ぶぶん表示は遅くなりやすいので、本当に毎回最新が必要な箇所に絞って使うのがコツです。逆に、明示的にキャッシュさせたいときは cache: "force-cache" を指定します。
tags でオンデマンドに再検証する
時間ベースの revalidate は手軽ですが、「データを更新した瞬間にキャッシュを新しくしたい」という用途には向きません。そこで使うのがタグによるオンデマンド再検証です。fetch に next.tags を付けてキャッシュへ名前を付けておき、データを更新するタイミングで revalidateTag を呼ぶと、そのタグが付いたキャッシュだけをまとめて無効化できます。
export default async function PostsPage() {
const res = await fetch("https://api.example.com/posts", {
// このキャッシュに "posts" というタグを付ける
next: { tags: ["posts"] },
});
const posts = await res.json();
return <pre>{JSON.stringify(posts, null, 2)}</pre>;
}
記事を投稿・編集する処理(Server Action や Route Handler)の中で revalidateTag("posts") を呼ぶと、"posts" タグの付いたキャッシュが破棄され、次のアクセスで最新データが取得されます。Server Action の例を見てみます。
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
await fetch("https://api.example.com/posts", {
method: "POST",
body: JSON.stringify({ title: formData.get("title") }),
});
// "posts" タグの付いたキャッシュだけを再検証する
revalidateTag("posts");
// パス単位で再検証したいときはこちら
// revalidatePath("/posts");
}
revalidateTag はタグ単位、revalidatePath は URL パス単位でキャッシュを無効化します。特定のデータに紐づくキャッシュだけを狙って更新したいならタグ、あるページ全体を丸ごと作り直したいならパスを使う、と考えると選びやすくなります。どちらも next/cache からインポートし、Server Action や Route Handler など「サーバー側で更新が起きる場所」で呼び出します。
ルートセグメント設定でページ全体を制御する
個々の fetch ではなく、ページ(ルートセグメント)全体のキャッシュ方針をまとめて決めることもできます。page.tsx や layout.tsx で特定の変数を export すると、そのセグメントの挙動を指定できます。
// このページ全体を 60 秒ごとに再検証する
export const revalidate = 60;
// このページを常に動的レンダリングにする
export const dynamic = "force-dynamic";
export default async function PostsPage() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <pre>{JSON.stringify(posts, null, 2)}</pre>;
}
export const revalidate = 60 はセグメント内の再検証間隔の基準になり、export const dynamic = "force-dynamic" はページを常に動的レンダリングにして、実質的に cache: "no-store" と同じ扱いにします。ページ内のすべての fetch を個別に設定するのが手間なときや、そのページの方針をひと目で分かるようにしたいときに便利です。なお、上の例は説明のために両方を並べていますが、revalidate(静的寄り)と dynamic = "force-dynamic"(動的)は方針が相反するため、実際にはどちらか一方だけを指定します。
Next.js 14 と 15 で既定の挙動が変わった
キャッシュを扱ううえで必ず押さえておきたいのが、バージョンによる既定値の違いです。オプションを何も指定しなかった fetch のデフォルト挙動が、Next.js 14 と 15 で変わりました。
| バージョン | オプション無しの fetch の既定 |
|---|---|
| Next.js 14 | キャッシュされる(force-cache 相当) |
| Next.js 15 | キャッシュされない(no-store 相当) |
Next.js 14 までは、オプションを付けない fetch は既定でキャッシュされ、明示的に cache: "no-store" を書いて初めて動的になりました。ところが Next.js 15 では既定が反転し、オプション無しの fetch はキャッシュされない(no-store 相当)ようになりました。つまり、同じコードでも 14 では静的、15 では動的、と挙動が変わり得ます。
この変更の実務上のポイントは、Next.js 15 でキャッシュを効かせたいなら、明示的に指定する必要があるということです。既定に頼らず、キャッシュしたい fetch には cache: "force-cache" か next: { revalidate: ... } を明示的に付けてください。逆に毎回取得したいときは cache: "no-store" を書いておけば、どちらのバージョンでも意図どおりに動きます。バージョンをまたいで扱うコードでは「既定に依存せず、キャッシュ方針を必ず明示する」のが安全です。
まとめ
App Router の Server Component では、fetch に渡すオプションでキャッシュ挙動を細かく制御できます。時間ベースで定期更新したいなら next: { revalidate: 60 } で ISR に、常に最新が必要なら cache: "no-store" で動的に、明示的にキャッシュしたいなら cache: "force-cache" を使います。データ更新のタイミングでキャッシュを新しくしたいときは、next: { tags: [...] } でタグを付けておき、revalidateTag や revalidatePath でオンデマンドに再検証します。ページ全体の方針は export const revalidate や export const dynamic で指定できます。そして最も注意したいのが、Next.js 14 では fetch が既定でキャッシュされたのに対し、Next.js 15 では既定が no-store(キャッシュされない)に変わった点です。バージョンをまたいでも意図どおり動くよう、キャッシュ方針は既定任せにせず必ず明示しておきましょう。