C
Next.js/レンダリング/Lesson 05

データフェッチング — fetchのキャッシュポリシー4種 + revalidate

35分·theory
このチャプター
2/5
TypeScript

データフェッチング — fetchのキャッシュポリシー4種 + revalidate

💡 なぜ学ぶのか? — 「キャッシュポリシー」がページ速度を決める

🎯 Pages Router の `getStaticProps`(SSG)・`getServerSideProps`(SSR)・ISR のオプションがすべて **fetch の単一オプション** に統合されました。
💼 fetch が自動的に重複排除・キャッシングされるため、「同じデータを複数のコンポーネントから呼び出すと非効率」という従来の懸念がなくなりました。
`revalidate`(時間ベース)と `tags`(手動無効化)の組み合わせにより、ページごとにデータの鮮度ポリシーを正確に設定できます。
🔗 これを理解していないと、すべてのページがデフォルトで SSG としてキャッシュされ「新しい記事を書いたのに表示されない」という問題が起きたり、逆に `no-store` を多用してサーバー負荷が増加したりします。
🏢 실무에서는
codemaster40 の学習コンテンツはほとんど変更されないため、SSG(`force-cache`)が適しています。一方、ユーザーの進捗やブックマークはユーザーごとに異なり頻繁に変わるため `no-store` が適切です。掲示板やコメントのようなコンテンツは、1〜5分の `revalidate` がバランスの取れた選択肢です。この判断を fetch の呼び出し単位で行えることが、App Router の大きな強みです。

fetchのキャッシュポリシー4種 + revalidateの方法

1. デフォルト — force-cache (SSG)

tsx
const res = await fetch('https://api/posts');
// ビルド時に一度だけfetch → 結果が永続的にキャッシュされる
// ページを再訪しても同じ結果(再デプロイまで)

高速ですが、データが変わっても反映されません。ほぼ変化しないデータに適しています。

2. no-store (SSR)

tsx
const res = await fetch('https://api/posts', {
  cache: 'no-store',
});
// リクエストのたびに新規fetch — Pages RouterのgetServerSidePropsと同等

常に最新。ユーザーごとのデータ・リアルタイム価格・在庫など、リクエストのたびに新鮮さが求められるデータに最適。

3. revalidate (ISR — Incremental Static Regeneration)

tsx
const res = await fetch('https://api/posts', {
  next: { revalidate: 60 },
});
// 60秒間キャッシュし、その後の次のリクエスト時にバックグラウンドで再生成

キャッシュの高速性と新鮮さを両立。ブログ・ドキュメント・頻繁には変わらないが時々更新されるコンテンツに最適。

4. tags + revalidateTag (手動無効化)

tsx
// データ取得 — タグを付与
const res = await fetch('https://api/posts', {
  next: { tags: ['posts'] },
});

// 別の場所(Server Actionなど)でタグを無効化
import { revalidateTag } from 'next/cache';
async function createPost(formData: FormData) {
  'use server';
  await db.post.create({ ... });
  revalidateTag('posts'); // 'posts'タグが付いたすべてのfetchキャッシュを無効化
}

突発的な更新(記事公開直後など)に最適。ISR + tagsが実務の標準的な組み合わせ。

5. パス単位の無効化 — revalidatePath

tsx
import { revalidatePath } from 'next/cache';
revalidatePath('/blog');     // /blogページのすべてのキャッシュを無効化
revalidatePath('/blog/[slug]', 'page'); // 動的パス全体

6. まとめ — いつ何を使うか

データの性質推奨ポリシー
ほぼ変わらない(学習コンテンツ・ランディングページ)force-cache(デフォルト)
数分単位の更新でOK(ブログ・ニュース)revalidate: 60~300
ユーザーごと・リアルタイムno-store
ユーザー操作直後に即時更新tags + revalidateTag
💻 🅰️ Pages Router — getStaticProps / getServerSidePropsを分けて定義
// ❌ Pages Router — モードごとに異なる関数

// 📁 pages/blog/index.tsx (SSG)
import type { GetStaticProps } from 'next';

interface Post { id: number; title: string; }

export const getStaticProps: GetStaticProps<{ posts: Post[] }> = async () => {
  const posts = await fetch('https://api/posts').then(r => r.json());
  return {
    props: { posts },
    revalidate: 60, // ISR — 60秒
  };
};

export default function Blog({ posts }: { posts: Post[] }) {
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

// 📁 pages/dashboard.tsx (SSR — リクエストごとに)
import type { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps<{ user: User }> = async (ctx) => {
  const user = await fetchUser(ctx.req.cookies.session);
  return { props: { user } };
};

export default function Dashboard({ user }: { user: User }) {
  return <h1>こんにちは、{user.name}</h1>;
}

// 欠点:
// - フェッチモードをページ単位で決定 (1ページ内で一部だけISRなどは不可)
// - データを props にシリアライズ → 大きなオブジェクトは非効率
💻 🅱️ App Router — fetchオプション一つで統合
// ✅ App Router — fetch オプションでキャッシュポリシーを決定

interface Post { id: number; title: string; }
interface User { id: number; name: string; }

// 📁 app/blog/page.tsx — ISR 60秒
export default async function Blog() {
  const posts: Post[] = await fetch('https://api/posts', {
    next: { revalidate: 60, tags: ['posts'] },
  }).then(r => r.json());

  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

// 📁 app/dashboard/page.tsx — SSR (リクエストごとに)
import { cookies } from 'next/headers';

export default async function Dashboard() {
  const session = cookies().get('session')?.value;
  const user: User = await fetch(`https://api/users/me`, {
    cache: 'no-store', // リクエストごと
    headers: { cookie: `session=${session}` },
  }).then(r => r.json());

  return <h1>こんにちは、{user.name}</h1>;
}

// 📁 app/mixed/page.tsx — 1ページ内で異なるポリシー
export default async function Mixed() {
  // 静的コンテンツ — 永続キャッシュ
  const intro = await fetch('https://api/intro').then(r => r.json());

  // 更新頻度の低いコンテンツ — 5分
  const posts = await fetch('https://api/posts', {
    next: { revalidate: 300 },
  }).then(r => r.json());

  // ユーザーデータ — 毎回
  const me = await fetch('https://api/me', {
    cache: 'no-store',
  }).then(r => r.json());

  return <Layout intro={intro} posts={posts} me={me} />;
}

// 📁 app/posts/actions.ts — 記事作成後にキャッシュを無効化
'use server';
import { revalidateTag } from 'next/cache';

export async function createPost(formData: FormData) {
  await db.post.create({
    data: { title: formData.get('title') as string },
  });
  revalidateTag('posts'); // tags: ['posts'] が付いた fetch キャッシュをすべて無効化
}

💡 💡 Next.jsデータフェッチング 実践5選

1. デフォルトはforce-cache — 指定しなければキャッシュされる
ビルド時にfetchして永続キャッシュ。データが変わっても表示が更新されない原因の第1位。

2. revalidate時間の決め方のヒューリスティック

  • コンテンツが1分に1回未満しか変わらない → 60
  • 1時間に数回 → 300
  • ほぼ変わらない → 3600以上または無制限(force-cache + tags)

3. tagsとrevalidateTagの組み合わせが最も強力
記事の作成・編集・削除アクションでrevalidateTag('posts')の一行だけで関連キャッシュをすべて無効化。

4. 同じURLは自動的に重複排除 — 呼び出し回数を気にしなくてOK
同じレンダリング内で複数のコンポーネントがfetch('/api/me')を呼んでも、実際のネットワークリクエストは1回だけ。

5. cookies()やheaders()を呼び出すと自動的にdynamicになる

tsx
import { cookies } from 'next/headers';
const c = cookies(); // この行があるとforce-cacheは無視され、毎リクエストSSRになる

fetchのキャッシュモードに関係なく、ページがdynamicモードに切り替わります。

⚡ 実際に試してみよう — キャッシュポリシーシミュレーター

各fetchのキャッシュポリシーがページの新鮮さにどう影響するかをシミュレーションします。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

Next.js App Routerで`fetch(url)`(オプションなし)を呼んだときのデフォルトのキャッシュ動作は何ですか?
💡 Next.js 13/14 における fetch のデフォルト値は `force-cache` です。オプションを指定せずに fetch を呼び出すと、ビルド時に一度だけ実行され、その結果が永続的にキャッシュされます(次のデプロイまで)。データを更新するには、`revalidate`・`tags`・`no-store` のいずれかを明示的に指定する必要があります。⚠️ Next.js 15 からデフォルト値が `no-store` に変更されるという RFC があるため、使用バージョンの確認が必要です。
データフェッチ — fetch キャッシュポリシー4種 - Next.js