3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js で多段キャッシュを設計する — CloudFront / Redis / TanStack Query を全部つなぐ

3
Last updated at Posted at 2026-06-09

Next.js で多段キャッシュを設計する — CloudFront / Redis / TanStack Query を全部つなぐ

対象: Next.js 16 (App Router) + React 19 でフロントを構築し、API バックエンドと組み合わせるプロジェクト
想定読者: 「キャッシュ層がいくつもあって、どこで何が効いているのか整理したい」人


忙しい人向けの結論

実運用では以下のレイヤーが重なります。

# レイヤー 場所 効く範囲
CloudFront (CDN) エッジ 静的アセット / 静的JSON / next/image 経由の最適化済み画像
Next.js Data Cache (use cache) サーバー(既定はプロセス内 in-memory LRU) use cache を付けた Server Component / 関数の結果
Next.js Image Cache サーバー(.next/cache/images <Image> で最適化した結果(WebP/AVIF)
Redis (Custom Cache Handler) Next.js の外側(共有ストア) 複数インスタンス間での use cache 共有 / invalidation 共有が必要なときの選択肢
TanStack Query クライアント ブラウザ内のサーバ状態
Browser HTTP Cache / Router Cache クライアント Cache-Control に従う静的リソース / 訪問済みルートの RSC ペイロード

この記事では番号 ①〜⑥ を全体で統一して使います(後述の本文見出し「レイヤー1〜6」と 1 対 1 で対応)。

「全部入れる」のではなく、データの鮮度要件・パーソナライズ有無・更新頻度 に応じて適切な層に乗せます。

📌 前提(Next.js 16): 本記事の Data Cache(use cache)は Cache Components 機能です。next.config.tscacheComponents: true を設定しないと use cache / cacheLife / cacheTag は一切機能しません(詳細はレイヤー2)。


前提:何をキャッシュして、何をキャッシュしないか

✅ キャッシュして良いもの

  • 公開コンテンツ(誰が見ても同じデータ)
    • SEO メタデータ(タイトル・OGP)
    • 公開記事・商品情報、お知らせ一覧
    • 静的画像・フォント・CSS/JS バンドル
  • 更新頻度が低いマスタデータ
    • 利用規約
  • 再計算コストが高い集計値
    • ダッシュボードの集計(鮮度を許容できる範囲で)

❌ キャッシュしてはいけないもの

  • 個人情報・PII
    • ログインユーザーのプロフィール、購入履歴、メールアドレス
    • CDN・サーバー共有キャッシュ・Redis など、他ユーザーに混入しうる全てのキャッシュ層に乗せない
    • クライアント側(TanStack Query 等のメモリ)で当該ユーザーセッション内に閉じて持つのは可
  • 権限依存のデータ
    • 管理者のみ閲覧可能なリスト、限定公開コンテンツ
  • トークン・セッション・パスコード
    • 認証情報そのもの
  • 下書き / プレビュー
    • 公開前データ。共有キャッシュをバイパスして API 直
  • リアルタイム性が必須
    • 在庫数、抽選結果、決済結果

⚠️ 注意が必要なもの

  • 「ログイン状態に応じて表示が変わるが、データ自体は公開」
    • 例: 誰が見ても同じ公開記事・商品ページのヘッダーに、「ログイン中のユーザー名」やカート個数だけを差し込む
    • 本文(公開データ)は CDN キャッシュ可。ユーザー固有の差し込みはクライアント側で(サーバー側でユーザー単位にキャッシュしたい場合は use cache: private という選択肢もある)

全体像

リクエストの流れと各キャッシュレイヤーの位置関係:

番号と対応レイヤー

# レイヤー 場所
CloudFront Edge Cache CDN(エッジ)
Next.js Data Cache (use cache) Next.js サーバー(既定は in-memory LRU)
Next.js Image Cache (.next/cache/images) Next.js サーバー
Redis (Custom Cache Handler) Next.js 外部の共有ストア
TanStack Query クライアント(メモリ)
Browser HTTP Cache / Router Cache クライアント(ブラウザ / Next.js 内部)

ポイントは Redis が API サーバー側ではなく、Next.js の use cache の "共有バックエンド" として配置される こと。これにより複数 ECS タスクで同じキャッシュを参照できます。ただし片方のタスクで revalidateTag() を呼んだ結果が他タスクに伝わるのは 自動ではなく、Cache Handler が共有ストア経由でタグ状態を同期する実装(分散タグ協調)を持つ場合に限ります(詳細はレイヤー4)。


レイヤー1: CloudFront (CDN) [1]

持ち方

CloudFront ビヘイビアでパスごとにオリジンを切り替えます。

パス オリジン TTL の考え方
/_next/static/* Next.js immutable(ファイル名にハッシュ)
/_next/image* Next.js minimumCacheTTL に従う
/api/* Next.js キャッシュしない(or 短TTL)
その他 Next.js SSR 結果。基本キャッシュしない

キャッシュ無効化

  • リリース時に CloudFront Invalidation を発行(/_next/image* など)
  • ただし invalidation は反映までラグがあるため、ファイル名へのハッシュ付与で実質 immutable にする方が望ましい

確認方法

ブラウザ DevTools の Network タブで該当リクエストのレスポンスヘッダーを確認:

x-cache: Hit from cloudfront
x-amz-cf-pop: NRT57-P1
age: 142
  • x-cache: Hit from cloudfront → エッジヒット
  • x-cache: Miss from cloudfront → オリジンに到達
  • age でキャッシュされてからの経過秒数

レイヤー2: Next.js Data Cache (use cache) [3]

Next.js 16 では "use cache" ディレクティブでサーバー側の結果をキャッシュできます。エントリは 既定でサーバープロセス内の in-memory LRU に保存されます。保存先を共有ストアに変えたい場合はレイヤー4の cacheHandlers / use cache: remote を使います。

⚠️ 前提: cacheComponents: true が必須 [2]
use cache / cacheLife / cacheTag は Next.js 16 の Cache Components 機能です。next.config.ts で有効化しないと、これらのディレクティブ・関数は一切機能しません。

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true, // ← これが無いと use cache は効かない
};

export default nextConfig;

cacheComponents は旧 experimental.dynamicIO / experimental.useCache / experimental.ppr を統合したフラグです。有効化すると Partial Prerendering (PPR) がデフォルト化し、データ取得がデフォルトで動的(dynamic by default) になります。「何をキャッシュするか」を use cache で明示的に選ぶモデルに変わるため、既存プロジェクトへ導入する際は影響範囲を確認してください。

"use cache" の置き場所は2通り

use cache を書ける場所は 「インライン(関数・コンポーネントの先頭)」「ファイル先頭」 の2通りです。インラインの場合、その関数が データを返せば関数キャッシュJSX を返せばコンポーネント(レンダリング結果)キャッシュになります(=同じインライン配置の2バリエーション)。

配置 書く場所 キャッシュ対象 使いどころ
インライン(関数) async 関数の先頭 関数の戻り値(データ) 単一のデータ取得を再利用したい
インライン(コンポーネント) Server Component の先頭 JSX のレンダリング結果 + 内部のデータ データ取得 + レンダリングコストごとキャッシュしたい
ファイル先頭 ファイルの先頭 そのファイルから export される全関数 データ取得モジュールをまとめてキャッシュ

以下、3つのパターンを順に見ていきます(①②がインライン、③がファイル先頭)。

① 関数単位(インライン) — 戻り値だけキャッシュ

import { cacheLife, cacheTag } from "next/cache";

export async function getProducts(category: string) {
  "use cache";
  cacheLife("shortTerm");
  cacheTag("products", `products-${category}`);

  const res = await fetch(`https://api.example.com/products?category=${category}`);
  return res.json();
}
  • 同じ引数 category で呼ばれたら キャッシュ済みの戻り値 を返す
  • 呼び出し側はサーバー / クライアント / 別関数のどこからでも OK
  • 「データ取得処理だけ重い」ケースに最適

② コンポーネント単位(インライン) — レンダリング結果ごとキャッシュ

// app/components/ProductList.tsx
import { cacheLife, cacheTag } from "next/cache";

type Product = { id: string; name: string; price: number };

export default async function ProductList({ category }: { category: string }) {
  "use cache";
  cacheLife("shortTerm");
  cacheTag("products", `products-${category}`);

  const products: Product[] = await fetch(
    `https://api.example.com/products?category=${category}`,
  ).then((r) => r.json());

  return (
    <ul className="product-list">
      {products.map((p) => (
        <li key={p.id}>
          <span>{p.name}</span><span>¥{p.price.toLocaleString()}</span>
        </li>
      ))}
    </ul>
  );
}
  • データ取得 + JSX のレンダリング結果(RSC ペイロード) をまとめてキャッシュ
  • 親ページから <ProductList category="shoes" /> のように呼ぶだけで、同じ category の組み合わせは即座に返る
  • レンダリングコスト(map / 計算 / フォーマット)が重い場合に効く

⚠️ コンポーネント単位のキャッシュはレンダリング結果を共有するので、ユーザーごとに異なる UI(ログイン名表示など)を含めてはいけない。引数(props)が同じなら誰が見ても同じ表示になる場合のみ使う。

また use cache を付けた関数・コンポーネントの 引数と戻り値は直列化可能(serializable)である必要があります。クラスインスタンスや関数は渡せません(children などは中身を参照しない pass-through なら可)。非直列化の props を渡すとビルドエラーになります。

③ ファイル先頭 — モジュールごとまとめてキャッシュ

// app/lib/posts.ts
"use cache";

import { cacheLife, cacheTag } from "next/cache";

export async function getPosts() {
  cacheLife("shortTerm");
  cacheTag("posts");
  return fetch("https://api.example.com/posts").then((r) => r.json());
}

export async function getPostById(id: string) {
  cacheLife("shortTerm");
  cacheTag("posts", `posts-${id}`);
  return fetch(`https://api.example.com/posts/${id}`).then((r) => r.json());
}
  • このファイル内の 全 export 関数 が自動的にキャッシュ対象
  • データ取得ユーティリティをひとつのファイルにまとめて管理しているケース向け

cacheLife プロファイル例

next.config.tsトップレベルで定義します(cacheComponents: true とセット。Next.js 15 時代の experimental.cacheLife ではありません):

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    realtime: { stale: 0, revalidate: 1, expire: 60 },
    shortTerm: { stale: 60, revalidate: 300, expire: 3600 },
    longTerm: { stale: 3600, revalidate: 86400, expire: 604800 },
  },
};

export default nextConfig;

各プロファイルは3つの値を持ちます。

  • stale: クライアント側でキャッシュをフレッシュとみなす秒数(この間は再検証しない)
  • revalidate: サーバー側でバックグラウンド再検証を行う間隔の秒数
  • expire: キャッシュが完全に失効するまでの秒数(これを過ぎると次のリクエストはフレッシュ取得を待つ)

⚠️ expirerevalidate より長い値にする必要があります。

タグでピンポイント破棄(updateTag / revalidateTag[4]

Next.js 16 では更新系のキャッシュ破棄 API が 2 つに整理されました。

API 用途 挙動
updateTag(tag) Server Action 内で「更新直後に最新値を見せたい」(read-your-writes) 即時失効。次リクエストはフレッシュ取得を待つ
revalidateTag(tag, profile) Route Handler / Webhook 等。多少の遅延は許容 stale-while-revalidate(次回アクセス時に背景で再取得)

⚠️ Next.js 16 では 単一引数の revalidateTag(tag) は非推奨(TS エラーになり将来削除の可能性)。第2引数に cacheLife プロファイル(例 "max")または { expire: 0 } を渡します。

"use server";

import { updateTag } from "next/cache";

type ProductInput = { name: string; price: number };

export async function updateProduct(category: string, payload: ProductInput) {
  await fetch(`https://api.example.com/products/${category}`, {
    method: "PUT",
    body: JSON.stringify(payload),
  });

  // Server Action: 更新直後に最新を見せる(read-your-writes)
  updateTag(`products-${category}`);
}

Route Handler / Webhook など、即時反映までは不要で背景再検証で良い場合:

import { revalidateTag } from "next/cache";

// stale-while-revalidate(次にこのタグのページが訪問されたとき更新)
revalidateTag(`products-${category}`, "max");
// 外部サービスから即時失効させたいなら
// revalidateTag(`products-${category}`, { expire: 0 });

Redis Custom Cache Handler を入れていれば、このタグ無効化を 他の ECS タスクにも反映できますが、それは Cache Handler が共有ストア経由でタグ状態を同期する実装を持つ場合に限ります(レイヤー4参照)。in-memory のままなら このタスク内のキャッシュだけ が破棄されます。

確認方法

ログで確認(use cache はこれが主軸)

use cache のエントリは既定で プロセス内 in-memory に乗るため、ファイルを覗いても見えません。ヒット/ミスは NEXT_PRIVATE_DEBUG_CACHE=1 のログで確認します:

NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev

保存先を Redis 等に変えた場合(レイヤー4の cacheHandlers / use cache: remote)は、そのストア側(例: redis-cli KEYS ...)で確認します。

.next/cache/ の中身(注意)

.next/cache/ 配下には主に次が書き出されます:

.next/cache/
├── fetch-cache/        # 旧 fetch ベースの Data Cache(fetch(url, { next: { revalidate } }))
├── images/             # 画像最適化キャッシュ
└── webpack/            # ビルドキャッシュ

fetch-cache/ は従来の fetch() / ISR キャッシュ用ディレクトリです。use cache のエントリはここには現れません(既定で in-memory のため)。use cache を題材にこのディレクトリを ls しても期待どおりに見えない点に注意してください。


レイヤー3: Next.js Image Cache(next/image[5]

<Image> を使うと Next.js サーバー側で画像を最適化(WebP/AVIF 変換、リサイズ)し、結果を .next/cache/images/ に保存します。

🎯 方針の前提: 画像は CloudFront を主役にする
画像配信は CloudFront(外側)を耐久キャッシュの主役にするのが第一選択です。CloudFront と Next.js Image Cache は「どちらか」ではなく役割が違います。

守るもの 効く場面
CloudFront エッジレイテンシ+オリジン到達そのもの ほぼ全リクエスト(HIT なら ECS に来ない)
Next.js Image Cache.next/cache/images/ オリジンの最適化CPU(sharp の再エンコード) CloudFront が MISS したときだけ

ECS / コンテナ運用では .next/cache/images/タスクごとに独立し、デプロイで揮発します。つまりこれを耐久層として当てにはできません。next/image の最適化を使う限りこのキャッシュは原則無効化できませんが、**「CloudFront が耐久層、.next/cache/images/ は CloudFront MISS 時にオリジンの再最適化を減らすベストエフォート」**と割り切るのは妥当な方針です。

持ち方

import Image from "next/image";

<Image src="/images/hero.png" width={1200} height={630} alt="..." />
  • リクエストごとに width/quality の組み合わせで最適化バリエーションが生成される
  • 結果は .next/cache/images/{hash}/{etag}.{ext} に保存
  • レスポンスは Cache-Control: public, max-age=... 付きで返るので CloudFront でもキャッシュ可能

TTL 設定

next.config.ts:

images: {
  minimumCacheTTL: 60 * 60 * 24 * 7, // 7日間
  formats: ["image/avif", "image/webp"],
  qualities: [75], // Next.js 16 では既定以外の quality を使うなら明示が必須
  remotePatterns: [
    { protocol: "https", hostname: "cdn.example.com" },
  ],
}
  • minimumCacheTTL.next/cache/images/ 側の保持期間を制御(Next.js 16 で既定が 60s14400s(4時間) に変更)
  • formats の既定は ["image/webp"] のみ。AVIF はオプトイン(上記のように明示)
  • Next.js 16 では quality prop を既定(75)以外で使う場合 images.qualities の指定が必須。未指定だと 400 になりうる
  • 外部画像は 必要最小限のドメインのみ remotePatterns に追加(セキュリティ重要)

unoptimized の使いどころ

  • 外部の既に最適化済みの画像、SVG、動的に変わる画像 → unoptimized
  • basePath を設定している環境では、ローカル画像(public/ 配下)に unoptimized を付けると basePath が付与されず 404 になることがある(basePath 未使用の環境では起きない)

確認方法

.next/cache/images/ を覗く

ls -la .next/cache/images/
# 各画像ごとにディレクトリができ、配下に最適化済みバイナリ + メタJSON

レスポンスヘッダー

DevTools で /_next/image?url=...&w=...&q=... リクエストを確認:

x-nextjs-cache: HIT
cache-control: public, max-age=604800, must-revalidate
content-type: image/avif
  • x-nextjs-cache: HIT.next/cache/images/ から配信
  • x-nextjs-cache: MISS → リクエスト時に最適化(次回以降は HIT)
  • x-nextjs-cache: STALE → TTL 切れだが再生成中

CloudFront との二段化

CloudFront 側で /_next/image* をキャッシュさせると、リクエストはこう流れます:

  1. ブラウザ → CloudFront(HIT) → 完結(ECS に来ない・最速)
  2. CloudFront(MISS) → Next.js(x-nextjs-cache: HIT) → CloudFront にキャッシュ(最適化は走らない)
  3. CloudFront(MISS) → Next.js(x-nextjs-cache: MISS) → 最適化処理(sharp)→ 配信 → 以降キャッシュ

CloudFront を主役にする場合、キャッシュキーの設定を正しくしないとヒット率が崩れる/事故る点に注意します。

  • キャッシュキーに url / w / q クエリ文字列を含める
    /_next/image?url=...&w=...&q=... はクエリで配信物が変わります。これらをキャッシュキーから外すと、別サイズ・別画像が混ざります。

  • ⚠️ フォーマット(Accept)をキャッシュキーに反映する(CDN キャッシュするなら必須) — 一番ハマる
    Next.js の画像最適化は リクエストの Accept ヘッダを見て AVIF / WebP を出し分け、レスポンスに Vary: Accept を付けます。ところが CloudFront は Vary を自動では尊重しないため、フォーマット情報をキャッシュキーに入れないと、WebP/AVIF 非対応ブラウザに AVIF を返すような事故が起きます。
    生の Accept はブラウザ/バージョンで多様なので、おすすめは CloudFront Function(viewer-request)で Accept を3値に正規化した独自ヘッダ x-img-format をキャッシュキーに使う方法です(キャッシュも3バケットに抑えられヒット率が良い):

    • Acceptimage/avif を含む → x-img-format: avif
    • Acceptimage/webp を含む → x-img-format: webp
    • それ以外(WebP/AVIF 非対応ブラウザ)→ x-img-format: original

    設定は 「キャッシュキー」と「オリジン転送」を分けるのがコツ:

    ポリシー 含めるもの
    Cache policy(キャッシュキー) 正規化済み x-img-formaturl / w / q
    Origin request policy(オリジン転送) 生の Accepturl / w / q(Next.js が実フォーマット判定に使う)
  • TTL は長めにminimumCacheTTL を長くするとオリジンの Cache-Control: max-age が長くなり、CloudFront が長く保持 → MISS 自体が減り、オリジンでの再最適化が減る。

  • invalidation — 同じ URL で元画像を差し替えた場合は CloudFront の invalidation が必要(/_next/image* 等)。


レイヤー4: Redis (Custom Cache Handler) — 共有が必要なときの選択肢

in-memory と Redis、どちらを使う?

use cache の既定の保存先は 各 Next.js プロセス内の in-memory LRU です。そのため Next.js を ECS / Kubernetes など複数インスタンスで運用 すると、各タスクがそれぞれ独立した in-memory キャッシュを持ち、インスタンス間で内容がズレうる(かつ再起動で消える)ということになります。

💡 前提として、以下は 常駐プロセス(next start / Docker / ECS) を想定しています。サーバーレス環境では in-memory がリクエスト間で永続しないため、共有が必要なら最初から use cache: remote / cacheHandlers が前提になります。

ただし「ズレたら必ず困る」わけではなく、要件次第で in-memory のままで十分なケースもあります。

状況 推奨
単一インスタンス運用 in-memory で十分
複数インスタンスだが、各タスクが個別に再フェッチしても許容できる in-memory + 短い TTL
キャッシュ生成コストが重い(DB集計・外部API多段呼び出し)ので無駄な再計算を避けたい Redis で共有
タグ無効化を全タスクに反映したい(在庫・公開フラグなど。分散タグ協調の実装が前提) Redis で共有
鮮度ズレが致命的(コンテンツ公開直後にどのタスクからアクセスしても同じ結果が必要) Redis で共有

判断軸はざっくり言うと:

  • 「タスク間でズレてもいいか」 → YES なら in-memory + 短TTL
  • 「即時反映 or 重い処理の再計算を避けたいか」 → YES なら Redis

in-memory のままで運用する場合

cacheLife を短めに設定してインスタンス間のズレを許容範囲に抑えます。レイヤー2で紹介した "use cache"(インライン / ファイル先頭)をそのまま使い、TTL だけ短く調整するのが基本です。

// 関数単位(in-memory・短TTL運用)
import { cacheLife, cacheTag } from "next/cache";

export async function getProducts(category: string) {
  "use cache";
  cacheLife("shortTerm"); // 例: 60秒程度で expire
  cacheTag("products", `products-${category}`);

  const res = await fetch(`https://api.example.com/products?category=${category}`);
  return res.json();
}
// コンポーネント単位(in-memory・短TTL運用)
export default async function FeaturedBanner() {
  "use cache";
  cacheLife("shortTerm");
  cacheTag("featured-banner");

  const banner = await fetch("https://api.example.com/featured").then((r) => r.json());

  return (
    <section className="banner">
      <h2>{banner.title}</h2>
      <p>{banner.subtitle}</p>
    </section>
  );
}

短TTLで「最大でも数十秒」のズレに留めれば、複数インスタンスでも実用上問題にならないケースは多いです。

Redis で共有する場合(Next.js 16 の正しいやり方) [6] [7]

ここが Next.js 16 で大きく変わったポイントです。混同しやすい 2 つの設定を区別してください。

設定キー 対象 用途
cacheHandler単数)+ cacheMaxMemorySize ISR / ルートハンドラ応答 / 旧 fetch Data Cache use cache には効きません
cacheHandlers複数, { default, remote } use cache / use cache: remote use cache の保存先を差し替えるのはこちら

つまり「単数 cacheHandler を設定するだけで use cache が Redis 共有になる」というのは 誤りです。use cache を複数インスタンスで共有したいなら、次の 2 つが必要です。

  1. next.config.tscacheComponents: true を有効化し、cacheHandlers.remote に共有ストア(Redis 等)の実装を登録する
  2. 共有したい関数・コンポーネントのコードに 'use cache: remote' ディレクティブを書く(= 「アプリコード変更不要」ではない。素の 'use cache' は in-memory 止まりで共有されない)
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheHandlers: {
    // 'use cache' 用(省略時は in-memory LRU)
    // default: require.resolve("./cache-handler-default.js"),
    // 'use cache: remote' 用(Redis 等の共有ストア)
    remote: require.resolve("./cache-handler-remote.js"),
  },
};

export default nextConfig;
// 共有したい関数には 'use cache: remote' を付ける
import { cacheLife, cacheTag } from "next/cache";

export async function getProducts(category: string) {
  "use cache: remote"; // ← 全インスタンスで共有される
  cacheLife("shortTerm");
  cacheTag("products", `products-${category}`);

  const res = await fetch(`https://api.example.com/products?category=${category}`);
  return res.json();
}

⚠️ ライブラリの対応状況に注意: かつて定番だった @neshca/cache-handlerNext.js 13.5〜14 までの対応(後継の @fortedigital/nextjs-cache-handler も執筆時点で Next.js 16 / use cache は部分対応)。@neshcaonCreation + redis-strings という旧 API は 単数 cacheHandler(ISR 用)向けで、use cache の共有には使えません。Next.js 16 で use cache: remote の Redis バックエンドを用意する場合は、公式 cacheHandlers リファレンスの ReadableStream 対応ハンドラ実装(get / set / refreshTags / getExpiration / updateTags)を実装します。

構造

                    ┌──────────────────┐
[ECS Task #1] ─────►│                  │
[ECS Task #2] ─────►│      Redis       │  ← タグ無効化を共有ストアで管理
[ECS Task #3] ─────►│  (共有キャッシュ) │
                    └──────────────────┘

すべてのタスクが同じ Redis を見るので:

  • どのタスクが処理しても キャッシュは1回だけ生成
  • タグ無効化を全タスクに反映できる。ただし自動ではなく、Cache Handler が updateTags()(無効化を共有ストアへ書く)と refreshTags()(各リクエスト前に共有ストアからタグ状態を同期)を実装している場合に限る(分散タグ協調)。これが無いと各タスクはローカルのタグ状態しか見ず、片方で破棄しても他タスクは stale を返し続けます。 [8]

注意点

  • PII を絶対に Redis 経由のキャッシュに乗せない(複数ユーザー間で共有されるため)
  • cacheTag 命名を雑にすると revalidateTag の範囲が広すぎてヒット率が落ちる
  • Redis 自体の TTL ではなく Next.js の cacheLife で expire を制御(二重TTLを避ける)
  • 接続エラー時のフォールバック挙動(in-memory に落とすか、ミス扱いにするか)を Cache Handler 側で設計する

確認方法

Redis CLI で直接覗く

redis-cli
> KEYS nextjs:*
> GET nextjs:abc123...
> MONITOR   # リアルタイムにキー操作を観察
  • nextjs: プレフィックスで Next.js のキャッシュキーが格納されている
  • cacheTag ごとのインデックスキーも作られる(実装による)

タグ無効化の伝播確認

  1. タスク#1 で revalidateTag('products', 'max')(または Server Action 内の updateTag('products'))を呼ぶ
  2. タスク#2 のログ(NEXT_PRIVATE_DEBUG_CACHE=1)で該当タグの再フェッチが走ることを確認
  3. Redis 上で対応するキー / タグ状態が更新されていることを確認

伝播するのは Cache Handler が分散タグ協調(updateTags / refreshTags)を実装している場合のみ。実装が無いと他タスクには反映されません。


レイヤー5: TanStack Query(クライアント) [9]

ブラウザ側でサーバー状態をキャッシュするライブラリ。Next.js の Server Component が主軸でも、クライアントでインタラクティブに再フェッチしたいケース で使います。

持ち方

"use client";

import { useQuery } from "@tanstack/react-query";

type Notification = { id: string; title: string };

export function NotificationList({ userId }: { userId: string }) {
  const { data } = useQuery({
    queryKey: ["notifications", userId],
    queryFn: (): Promise<Notification[]> =>
      fetch(`/api/notifications/${userId}`).then((r) => r.json()),
    staleTime: 60_000,      // 60秒間は再フェッチ不要
    gcTime: 5 * 60_000,     // 5分間メモリ保持
  });
  return (
    <ul>
      {data?.map((n) => (
        <li key={n.id}>{n.title}</li>
      ))}
    </ul>
  );
}

staleTime / gcTime は TanStack Query v5 の API です(v4 の cacheTime は v5 で gcTime に改名)。

Server Component との使い分け

用途 使うもの
初回描画用のサーバ取得 Server Component + use cache
ユーザー操作で再フェッチ・楽観更新 TanStack Query
リアルタイム性が必要(無限スクロール等) TanStack Query

注意点

  • PII を queryKey に含めないこと(DevTools で丸見えになる)
  • API 通信は必ず Next.js サーバー経由(Route Handler)にして、クライアントから直接バックエンドを叩かない

確認方法

React Query DevTools

"use client";

import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

画面右下のフローティングボタンから、現在保持しているクエリ一覧と状態(fresh / stale / inactive / fetching)を確認できます。


レイヤー6: Browser HTTP Cache & Router Cache

Browser HTTP Cache

Cache-Control / ETag に従ってブラウザがメモリ・ディスクに保持。Next.js のレスポンスヘッダーを正しく設定すれば自動で効きます。

DevTools の Network タブで:

  • Size 列が (disk cache) / (memory cache) → ブラウザキャッシュヒット
  • Status 列が 304 Not Modified → ETag 検証で再利用

Next.js Router Cache

App Router のクライアント側ナビゲーションで、訪問済みルートのRSCペイロードをキャッシュ。router.refresh() で破棄。明示的な設定はほぼ不要ですが、「戻る」で古いデータが見える原因になることがあるので存在を知っておくと良いです。


キャッシュ確認チートシート

確認したいもの 方法
CloudFront ヒット DevTools Network: x-cache: Hit from cloudfront
Next.js Data Cache (use cache) NEXT_PRIVATE_DEBUG_CACHE=1 のログ(既定は in-memory なのでファイルには現れない)
Next.js Image Cache .next/cache/images/ / Response header x-nextjs-cache: HIT
Redis (Cache Handler) redis-cli KEYS ... / MONITOR
TanStack Query React Query DevTools
Browser HTTP Cache DevTools Network: Size列 (disk cache) / Status 304

まとめ

  • キャッシュは多段で重ねる: ①CDN → ②use cache / ③Image Cache → ④Redis(共有) → API → DB
  • use cache を使うには cacheComponents: true が必須(Next.js 16 の Cache Components 機能)
  • Redis は API サーバー側ではなく、use cache の共有バックエンドとして使う
    • 共有は単数 cacheHandler ではなく use cache: remote + cacheHandlers で行う(コードに 'use cache: remote' を書く)
    • 「タスク間でズレが許せない」「再計算コストが重い」なら共有、そうでなければ in-memory + 短 TTL で十分
    • タグ無効化の他タスク伝播は 自動ではなく、分散タグ協調(updateTags/refreshTags)の実装が前提
  • PII・下書き・認証情報は共有キャッシュに乗せない
  • use cache の既定保存先はプロセス内 in-memory LRU.next/cache/fetch-cache/ は旧 fetch/ISR 用で、ここには現れない)
    • images/ = Image Cache
  • 確認はレスポンスヘッダー / ログ
    • x-cache (CloudFront)
    • x-nextjs-cache (Next.js Image)
    • NEXT_PRIVATE_DEBUG_CACHE=1 のログ (use cache Data Cache)
    • DevTools の Size列・Status 304 (ブラウザ)
  • クライアント側状態は TanStack Query、Server Component と役割分担する
  • 更新系は Next.js 16 の updateTag(read-your-writes)/ revalidateTag(tag, profile)(SWR)を使い分ける

正しく組めば、ユーザーへのレイテンシを抑えつつバックエンドの負荷も最小化できます。各レイヤーが「何を守っているのか」を意識して設計しましょう。


参考リンク

本文中の [n] は以下の番号に対応します。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?