nextjs-cache-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

How to Use This Skill

如何使用该技能

Read the user input below, then apply every rule and template in this file to their actual project. Replace all placeholders (
[Entity]
,
[collection]
, etc.) with names from their codebase before writing any code.
text
$ARGUMENTS

阅读下方用户输入,然后将本文件中的所有规则和模板应用到他们的实际项目中。 在编写任何代码之前,将所有占位符(
[Entity]
[collection]
等)替换为他们代码库中的名称。
text
$ARGUMENTS

Mental Model — Read This First

思维模型 — 请先阅读

Caching in Next.js 16 is architecture, not optimization. Design it upfront.
Layer 1 — Static shell
  Synchronous layout, nav, headers. No data fetching. Prerendered at build time.

Layer 2 — Cached shared content
  Same output for all users. Uses "use cache" + collection tag + cacheLife().
  Revalidated in the background. Wrapped in <Suspense>.

Layer 3 — Auto-scoped entity / filtered content
  Per-item pages or search results. Auto-keyed by arguments and closures.
  Entity tag added only when a mutation needs surgical invalidation.

Layer 4 — Dynamic personalized content
  User-specific. Reads cookies/headers OUTSIDE the cache boundary.
  Passes derived primitives as props into a nested cached component.
  Wrapped in <Suspense>.

Layer 5 — Invalidation
  Mutations call revalidation utilities only.
  Utilities call updateTag() — collection tags for bulk, entity tags for surgical.
Next.js 16中的缓存是架构层面的设计,而非优化手段。请提前进行设计。
第一层 — 静态外壳
  同步布局、导航栏、页眉。无数据获取。在构建时预渲染。

第二层 — 缓存的共享内容
  所有用户看到相同输出。使用"use cache" + 集合标签 + cacheLife()。
  在后台重新验证。用<Suspense>包裹。

第三层 — 自动作用域的实体/过滤内容
  单个条目页面或搜索结果。通过参数和闭包自动生成键。
  仅当需要精确失效的变更操作时,才添加实体标签。

第四层 — 动态个性化内容
  用户专属内容。在缓存边界之外读取cookies/headers。
  将派生的基础类型作为props传递给嵌套的缓存组件。
  用<Suspense>包裹。

第五层 — 失效机制
  变更操作仅调用重新验证工具。
  工具调用updateTag() — 集合标签用于批量失效,实体标签用于精确失效。

The single decision that drives everything else

决定一切的核心判断

Before adding cacheTag(CACHE_TAGS.[entity](id)):
  "Will a mutation ever call updateTag() on this specific entry individually?"
  YES → add entity tag factory to registry, tag the fetch, wire the revalidation utility
  NO  → auto-keying handles scoping; only the collection tag is needed

在添加cacheTag(CACHE_TAGS.[entity](id))之前:
  "是否存在某个变更操作会单独调用updateTag()来处理这个特定条目?"
  是 → 将实体标签工厂添加到注册表,为请求添加标签,连接重新验证工具
  否 → 自动键控已处理作用域;仅需集合标签即可

Core Concepts

核心概念

Auto cache key generation

自动缓存键生成

Next.js generates a unique cache key for every
"use cache"
function automatically. You never construct cache keys manually.
ComponentWhat it includes
Build IDChanges on every deploy — all caches invalidated automatically
Function IDHash of the function's file path and position in source
ArgumentsEvery value passed to the function at call time
Closure variablesEvery outer-scope value captured by the function
tsx
// Every unique resourceId produces a separate cache entry — automatically
async function Parent({ resourceId }: { resourceId: string }) {
  const fetchData = async (filter: string) => {
    "use cache";
    // key = [buildId] + [fn hash] + resourceId (closure) + filter (argument)
    return fetch(`/api/resources/${resourceId}?filter=${filter}`);
  };
  return fetchData("active");
}
Tags are for invalidation. Auto-keying handles scoping. These are different concerns.
Next.js会自动为每个
"use cache"
函数生成唯一的缓存键。 你无需手动构建缓存键。
组件包含内容
Build ID每次部署都会变更 — 所有缓存自动失效
Function ID函数文件路径和在源码中位置的哈希值
参数调用函数时传入的所有值
闭包变量函数捕获的所有外部作用域值
tsx
// 每个唯一的resourceId都会自动生成单独的缓存条目
async function Parent({ resourceId }: { resourceId: string }) {
  const fetchData = async (filter: string) => {
    "use cache";
    // 键 = [buildId] + [函数哈希] + resourceId(闭包) + filter(参数)
    return fetch(`/api/resources/${resourceId}?filter=${filter}`);
  };
  return fetchData("active");
}
标签用于失效。自动键控用于作用域划分。这是两个不同的关注点。

"use cache" placement rules

"use cache" 放置规则

PlacementWhen to use
Top of an async function bodySingle data-fetching function
Top of an async Server Component bodyEntire component output is cacheable
Never in a page componentPage components orchestrate; they do not fetch
"use cache"
must be the first statement in the function body — before any
await
.
ts
// CORRECT
async function getItems() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.items);
  return db.items.findMany();
}

// WRONG — directive after await
async function getItem(id: string) {
  const row = await db.items.findUnique({ where: { id } });
  "use cache"; // ignored — too late
  return row;
}
放置位置使用场景
异步函数体顶部单一数据获取函数
异步Server组件体顶部整个组件输出可缓存
绝不在页面组件中页面组件负责编排;不进行数据获取
"use cache"
必须是函数体中的第一个语句 — 在任何
await
之前。
ts
// 正确写法
async function getItems() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.items);
  return db.items.findMany();
}

// 错误写法 — 指令在await之后
async function getItem(id: string) {
  const row = await db.items.findUnique({ where: { id } });
  "use cache"; // 被忽略 — 时机太晚
  return row;
}

Closure variable rules

闭包变量规则

Keep closure variables to serializable primitives:
string
,
number
,
boolean
, plain objects, arrays.
tsx
// BAD — large object serialized into cache key
async function Parent({ item }: { item: Item }) {
  const fetch = async () => {
    "use cache";
    return getItem(item.id); // item is the whole object in closure
  };
}

// GOOD — extract only the primitive needed
async function Parent({ item }: { item: Item }) {
  const id = item.id;
  const fetch = async () => {
    "use cache";
    return getItem(id); // only a string in closure
  };
}
Next.js throws at runtime if a closure variable is non-serializable: class instances, functions, Symbols, circular references — all forbidden.

闭包变量应保持为可序列化的基础类型:
string
number
boolean
、普通对象、数组。
tsx
// 错误 — 大对象被序列化到缓存键中
async function Parent({ item }: { item: Item }) {
  const fetch = async () => {
    "use cache";
    return getItem(item.id); // item是整个对象,被包含在闭包中
  };
}

// 正确 — 仅提取所需的基础类型
async function Parent({ item }: { item: Item }) {
  const id = item.id;
  const fetch = async () => {
    "use cache";
    return getItem(id); // 闭包中仅包含字符串
  };
}
如果闭包变量不可序列化,Next.js会在运行时抛出错误: 类实例、函数、Symbols、循环引用 — 均被禁止。

Step 1 — Enable Cache Components

步骤1 — 启用缓存组件

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

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
Import these at the top of every file that uses caching:
ts
import { cacheLife, cacheTag, updateTag } from "next/cache";

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

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
在每个使用缓存的文件顶部导入以下内容:
ts
import { cacheLife, cacheTag, updateTag } from "next/cache";

Step 2 — Build the Cache Tag Registry

步骤2 — 构建缓存标签注册表

File:
lib/cache/tags.ts
Single source of truth for all tag strings. Raw tag strings are never written anywhere else.
文件
lib/cache/tags.ts
所有标签字符串的唯一来源。原始标签字符串绝不能写在其他任何地方。

Rules

规则

  • Lowercase only
  • Entity tags use
    domain:id
    format
  • Match your actual data model — do not invent names
  • Do not add entity factories speculatively — only when a mutation requires
    updateTag()
    on that entry
  • 仅使用小写
  • 实体标签使用
    domain:id
    格式
  • 匹配实际数据模型 — 不要凭空命名
  • 不要随意添加实体工厂 — 仅当变更操作需要对该条目调用
    updateTag()
    时才添加

Template

模板

ts
// lib/cache/tags.ts

export const CACHE_TAGS = {
  // COLLECTION TAGS — one per logical data group, always present
  [collection]: "[collection]",
  [anotherCollection]: "[anotherCollection]",

  // ENTITY TAG FACTORIES — only when a mutation targets a single entry via updateTag()
  [entity]: (id: string | number) => `[entity]:${id}`,
} as const;
ts
// lib/cache/tags.ts

export const CACHE_TAGS = {
  // 集合标签 — 每个逻辑数据组对应一个,始终存在
  [collection]: "[collection]",
  [anotherCollection]: "[anotherCollection]",

  // 实体标签工厂 — 仅当变更操作通过updateTag()针对单个条目时使用
  [entity]: (id: string | number) => `[entity]:${id}`,
} as const;

Example

示例

ts
// lib/cache/tags.ts

export const CACHE_TAGS = {
  // Collection tags — always present
  products: "products",
  categories: "categories",
  users: "users",

  // Entity tag factories — only where surgical invalidation is needed
  product: (id: string | number) => `product:${id}`,
  // "category" and "user" omitted — no mutation targets a single entry individually
} as const;

ts
// lib/cache/tags.ts

export const CACHE_TAGS = {
  // 集合标签 — 始终存在
  products: "products",
  categories: "categories",
  users: "users",

  // 实体标签工厂 — 仅在需要精确失效的场景下使用
  product: (id: string | number) => `product:${id}`,
  // 省略"category"和"user" — 没有变更操作会单独针对单个条目
} as const;

Step 3 — Build Revalidation Utilities

步骤3 — 构建重新验证工具

File:
lib/cache/revalidate.ts
All
updateTag()
calls live here. Mutations import these functions — they never call
updateTag()
directly.
文件
lib/cache/revalidate.ts
所有
updateTag()
调用都放在此处。变更操作导入这些函数 — 绝不直接调用
updateTag()

Template

模板

ts
// lib/cache/revalidate.ts
"use server";

import { updateTag } from "next/cache";
import { CACHE_TAGS } from "./tags";

function invalidateTags(tags: string[]) {
  for (const tag of tags) updateTag(tag);
}

// Bulk — any entry in the collection changed
export async function revalidate[Collection]Cache() {
  invalidateTags([CACHE_TAGS.[collection]]);
}

// Surgical — one specific entry changed
// Only write this if CACHE_TAGS.[entity] factory exists in the registry
export async function revalidate[Entity]Cache(id: string | number) {
  invalidateTags([
    CACHE_TAGS.[collection], // always invalidate the parent collection too
    CACHE_TAGS.[entity](id),
  ]);
}
ts
// lib/cache/revalidate.ts
"use server";

import { updateTag } from "next/cache";
import { CACHE_TAGS } from "./tags";

function invalidateTags(tags: string[]) {
  for (const tag of tags) updateTag(tag);
}

// 批量失效 — 集合中的任意条目发生变更
export async function revalidate[Collection]Cache() {
  invalidateTags([CACHE_TAGS.[collection]]);
}

// 精确失效 — 单个特定条目发生变更
// 仅当注册表中存在CACHE_TAGS.[entity]工厂时才编写此函数
export async function revalidate[Entity]Cache(id: string | number) {
  invalidateTags([
    CACHE_TAGS.[collection], // 始终同时失效父集合
    CACHE_TAGS.[entity](id),
  ]);
}

Example

示例

ts
// lib/cache/revalidate.ts
"use server";

import { updateTag } from "next/cache";
import { CACHE_TAGS } from "./tags";

function invalidateTags(tags: string[]) {
  for (const tag of tags) updateTag(tag);
}

export async function revalidateProductsCache() {
  invalidateTags([CACHE_TAGS.products]);
}

export async function revalidateProductCache(id: string | number) {
  invalidateTags([CACHE_TAGS.products, CACHE_TAGS.product(id)]);
}

export async function revalidateCategoriesCache() {
  invalidateTags([CACHE_TAGS.categories]);
}

ts
// lib/cache/revalidate.ts
"use server";

import { updateTag } from "next/cache";
import { CACHE_TAGS } from "./tags";

function invalidateTags(tags: string[]) {
  for (const tag of tags) updateTag(tag);
}

export async function revalidateProductsCache() {
  invalidateTags([CACHE_TAGS.products]);
}

export async function revalidateProductCache(id: string | number) {
  invalidateTags([CACHE_TAGS.products, CACHE_TAGS.product(id)]);
}

export async function revalidateCategoriesCache() {
  invalidateTags([CACHE_TAGS.categories]);
}

Step 4 — Implement Data Fetching

步骤4 — 实现数据获取

Place
"use cache"
in data-fetching functions. Never fetch inside page components.
"use cache"
放在数据获取函数中。绝不在页面组件中进行数据获取。

Collection fetch

集合数据获取

ts
// lib/data/[domain].ts

export async function get[Collection]() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);

  const res = await fetch(`${BASE_URL}/[endpoint]`);
  return res.json();
}
ts
// lib/data/[domain].ts

export async function get[Collection]() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);

  const res = await fetch(`${BASE_URL}/[endpoint]`);
  return res.json();
}

Entity fetch

实体数据获取

ts
export async function get[Entity](id: string) {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);
  // Add CACHE_TAGS.[entity](id) only if a mutation calls updateTag on this specific entry

  const res = await fetch(`${BASE_URL}/[endpoint]/${id}`);
  return res.json();
}
ts
export async function get[Entity](id: string) {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);
  // 仅当存在针对该特定条目调用updateTag的变更操作时,才添加CACHE_TAGS.[entity](id)

  const res = await fetch(`${BASE_URL}/[endpoint]/${id}`);
  return res.json();
}

What not to do

禁止操作

tsx
// WRONG — fetching in page component bypasses caching and invalidation
export default async function Page() {
  const res = await fetch("/api/items");
  const data = await res.json();
  return <View data={data} />;
}

tsx
// 错误 — 在页面组件中获取数据会绕过缓存和失效机制
export default async function Page() {
  const res = await fetch("/api/items");
  const data = await res.json();
  return <View data={data} />;
}

Step 5 — Structure Rendering Boundaries

步骤5 — 构建渲染边界

Every page follows this pattern:
Page component (sync, orchestration only — no data fetching)
  ├── Static shell (layout, nav — no data)
  ├── <Suspense> → Cached shared content
  └── <Suspense> → Dynamic personalized content
每个页面都遵循以下模式:
页面组件(同步,仅负责编排 — 不进行数据获取)
  ├── 静态外壳(布局、导航 — 无数据)
  ├── <Suspense> → 缓存的共享内容
  └── <Suspense> → 动态个性化内容

Standard page

标准页面

tsx
// app/[route]/page.tsx
import { Suspense } from "react";

export default function AnyPage() {
  return (
    <>
      <StaticShell />

      <Suspense fallback={<SharedSkeleton />}>
        <SharedContent />
      </Suspense>

      <Suspense fallback={<PersonalizedSkeleton />}>
        <PersonalizedSection />
      </Suspense>
    </>
  );
}

async function SharedContent() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);

  const data = await get[Collection]();
  return <[Collection]List data={data} />;
}
tsx
// app/[route]/page.tsx
import { Suspense } from "react";

export default function AnyPage() {
  return (
    <>
      <StaticShell />

      <Suspense fallback={<SharedSkeleton />}>
        <SharedContent />
      </Suspense>

      <Suspense fallback={<PersonalizedSkeleton />}>
        <PersonalizedSection />
      </Suspense>
    </>
  );
}

async function SharedContent() {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);

  const data = await get[Collection]();
  return <[Collection]List data={data} />;
}

Dynamic route page

动态路由页面

tsx
// app/[domain]/[id]/page.tsx
import { Suspense } from "react";

export default function EntityPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  return (
    <Suspense fallback={<EntitySkeleton />}>
      <EntityDetail params={params} />
    </Suspense>
  );
}

async function EntityDetail({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <CachedEntityView id={id} />;
}

async function CachedEntityView({ id }: { id: string }) {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);
  // Add CACHE_TAGS.[entity](id) only if a mutation needs surgical invalidation

  const item = await get[Entity](id);
  return <[Entity]View item={item} />;
}
tsx
// app/[domain]/[id]/page.tsx
import { Suspense } from "react";

export default function EntityPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  return (
    <Suspense fallback={<EntitySkeleton />}>
      <EntityDetail params={params} />
    </Suspense>
  );
}

async function EntityDetail({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <CachedEntityView id={id} />;
}

async function CachedEntityView({ id }: { id: string }) {
  "use cache";
  cacheLife("hours");
  cacheTag(CACHE_TAGS.[collection]);
  // 仅当变更操作需要精确失效时,才添加CACHE_TAGS.[entity](id)

  const item = await get[Entity](id);
  return <[Entity]View item={item} />;
}

Filtered / search params page

带过滤/搜索参数的页面

tsx
// app/[route]/page.tsx
import { Suspense } from "react";
import SuspenseOnSearchParams from "@/components/SuspenseOnSearchParams";

export default function FilteredPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string>>;
}) {
  return (
    <SuspenseOnSearchParams fallback={<FilteredListSkeleton />}>
      <FilteredList searchParams={searchParams} />
    </SuspenseOnSearchParams>
  );
}

async function FilteredList({
  searchParams,
}: {
  searchParams: Promise<Record<string, string>>;
}) {
  "use cache";
  cacheLife("minutes");
  cacheTag(CACHE_TAGS.[collection]);
  // searchParams is an argument → auto-keyed per unique param combination

  const { q = "", page = "1" } = await searchParams;
  return await get[Collection]ByFilter(q, page);
}
tsx
// app/[route]/page.tsx
import { Suspense } from "react";
import SuspenseOnSearchParams from "@/components/SuspenseOnSearchParams";

export default function FilteredPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string>>;
}) {
  return (
    <SuspenseOnSearchParams fallback={<FilteredListSkeleton />}>
      <FilteredList searchParams={searchParams} />
    </SuspenseOnSearchParams>
  );
}

async function FilteredList({
  searchParams,
}: {
  searchParams: Promise<Record<string, string>>;
}) {
  "use cache";
  cacheLife("minutes");
  cacheTag(CACHE_TAGS.[collection]);
  // searchParams是参数 → 针对每个唯一的参数组合自动生成键

  const { q = "", page = "1" } = await searchParams;
  return await get[Collection]ByFilter(q, page);
}

SuspenseOnSearchParams — required for filtered pages

SuspenseOnSearchParams — 带过滤参数页面的必备组件

Standard
<Suspense>
does not re-trigger its fallback on client-side navigation when only
searchParams
changes. Use this wrapper on every page with search or filter params.
tsx
// components/SuspenseOnSearchParams.tsx
"use client";

import { useSearchParams } from "next/navigation";
import { Suspense } from "react";

export default function SuspenseOnSearchParams({
  fallback,
  children,
}: {
  fallback: React.ReactNode;
  children: React.ReactNode;
}) {
  const searchParams = useSearchParams();
  return (
    <Suspense key={searchParams.toString()} fallback={fallback}>
      {children}
    </Suspense>
  );
}

标准<Suspense>在仅
searchParams
变更的客户端导航中不会重新触发fallback。 在所有带搜索或过滤参数的页面中使用此包装器。
tsx
// components/SuspenseOnSearchParams.tsx
"use client";

import { useSearchParams } from "next/navigation";
import { Suspense } from "react";

export default function SuspenseOnSearchParams({
  fallback,
  children,
}: {
  fallback: React.ReactNode;
  children: React.ReactNode;
}) {
  const searchParams = useSearchParams();
  return (
    <Suspense key={searchParams.toString()} fallback={fallback}>
      {children}
    </Suspense>
  );
}

Step 6 — Handle Personalized Content

步骤6 — 处理个性化内容

Never call
cookies()
,
headers()
, or
auth()
inside a
"use cache"
function. Read them outside the cache boundary and pass derived primitives as props.
tsx
// CORRECT

// 1. Read request-time data outside the cache boundary
async function PersonalizedSection() {
  const cookieStore = await cookies();
  const userId = cookieStore.get("userId")?.value;

  // 2. Pass as prop — auto-included in cache key
  return <CachedPersonalizedView userId={userId} />;
}

// 3. Cache the stable rendering
async function CachedPersonalizedView({
  userId,
}: {
  userId: string | undefined;
}) {
  "use cache";
  cacheLife("minutes");
  // userId is an argument → auto-keyed per user

  const data = await getPersonalizedData(userId);
  return <div>{/* render */}</div>;
}
tsx
// WRONG — dynamic API inside cached function
async function CachedView() {
  "use cache";
  const cookieStore = await cookies(); // throws or produces incorrect behavior
  return <div />;
}
绝不在
"use cache"
函数中调用
cookies()
headers()
auth()
。 在缓存边界之外读取这些内容,并将派生的基础类型作为props传递。
tsx
// 正确写法

// 1. 在缓存边界之外读取请求时的数据
async function PersonalizedSection() {
  const cookieStore = await cookies();
  const userId = cookieStore.get("userId")?.value;

  // 2. 作为props传递 — 自动包含在缓存键中
  return <CachedPersonalizedView userId={userId} />;
}

// 3. 缓存稳定的渲染结果
async function CachedPersonalizedView({
  userId,
}: {
  userId: string | undefined;
}) {
  "use cache";
  cacheLife("minutes");
  // userId是参数 → 针对每个用户自动生成键

  const data = await getPersonalizedData(userId);
  return <div>{/* 渲染内容 */}</div>;
}
tsx
// 错误写法 — 缓存函数中包含动态API
async function CachedView() {
  "use cache";
  const cookieStore = await cookies(); // 会抛出错误或产生异常行为
  return <div />;
}

Exception:
"use cache: private"

例外情况:
"use cache: private"

Use only when compliance requirements prevent refactoring to the pattern above:
tsx
async function getData() {
  "use cache: private";
  const session = (await cookies()).get("session")?.value; // allowed
  return fetchData(session);
}

仅当合规要求无法重构为上述模式时使用:
tsx
async function getData() {
  "use cache: private";
  const session = (await cookies()).get("session")?.value; // 允许使用
  return fetchData(session);
}

Step 7 — Wire Mutations to Invalidation

步骤7 — 连接变更操作与失效机制

Mutations call revalidation utilities. They never call
updateTag()
directly.
ts
// app/actions/[domain].ts
"use server";

import {
  revalidate[Collection]Cache,
  revalidate[Entity]Cache,
} from "@/lib/cache/revalidate";

export async function create[Entity](payload: unknown) {
  await db.[entity].create(payload);
  await revalidate[Collection]Cache();
}

export async function update[Entity](id: string | number, payload: unknown) {
  await db.[entity].update(id, payload);
  await revalidate[Entity]Cache(id);
}

export async function delete[Entity](id: string | number) {
  await db.[entity].delete(id);
  await revalidate[Entity]Cache(id);
}
ts
// WRONG — updateTag scattered in business logic, raw strings
export async function update[Entity](id: string, payload: unknown) {
  await db.[entity].update(id, payload);
  updateTag("[collection]");
  updateTag(`[entity]:${id}`);
}

变更操作调用重新验证工具。绝不直接调用
updateTag()
ts
// app/actions/[domain].ts
"use server";

import {
  revalidate[Collection]Cache,
  revalidate[Entity]Cache,
} from "@/lib/cache/revalidate";

export async function create[Entity](payload: unknown) {
  await db.[entity].create(payload);
  await revalidate[Collection]Cache();
}

export async function update[Entity](id: string | number, payload: unknown) {
  await db.[entity].update(id, payload);
  await revalidate[Entity]Cache(id);
}

export async function delete[Entity](id: string | number) {
  await db.[entity].delete(id);
  await revalidate[Entity]Cache(id);
}
ts
// 错误写法 — updateTag分散在业务逻辑中,使用原始字符串
export async function update[Entity](id: string, payload: unknown) {
  await db.[entity].update(id, payload);
  updateTag("[collection]");
  updateTag(`[entity]:${id}`);
}

Cache Duration Reference

缓存时长参考

ProfileUse when
"seconds"
Near-real-time data (live feeds, counters)
"minutes"
Frequently updated content (dashboards, notifications)
"hours"
Moderately stable content (listings, articles, configs)
"days"
Rarely updated content (reference data, documentation)
"max"
Effectively permanent (build-time constants, static assets)
For fine-grained control:
ts
cacheLife({
  stale: 3600,      // serve stale for up to 1 hour
  revalidate: 7200, // background revalidation every 2 hours
  expire: 86400,    // hard expiration at 1 day
});

配置使用场景
"seconds"
近实时数据(实时信息流、计数器)
"minutes"
频繁更新的内容(仪表盘、通知)
"hours"
中度稳定的内容(列表、文章、配置)
"days"
极少更新的内容(参考数据、文档)
"max"
近乎永久有效(构建时常量、静态资源)
如需细粒度控制:
ts
cacheLife({
  stale: 3600,      // 最多1小时内返回过期内容
  revalidate: 7200, // 每2小时进行一次后台重新验证
  expire: 86400,    // 1天后强制过期
});

Limitations

限制条件

  • Edge runtime is not supported — requires Node.js
  • Static export (
    output: "export"
    ) is not supported
  • Math.random()
    and
    Date.now()
    inside
    "use cache"
    execute once at build time, not per request
For request-time non-determinism:
tsx
import { connection } from "next/server";

async function DynamicContent() {
  await connection(); // defers execution to request time
  const id = crypto.randomUUID(); // different per request
  return <div>{id}</div>;
}

  • 不支持Edge运行时 — 需要Node.js
  • 不支持静态导出(
    output: "export"
  • "use cache"
    中的
    Math.random()
    Date.now()
    仅在构建时执行一次,而非每次请求
如需请求时的非确定性处理:
tsx
import { connection } from "next/server";

async function DynamicContent() {
  await connection(); // 将执行延迟到请求时
  const id = crypto.randomUUID(); // 每次请求生成不同值
  return <div>{id}</div>;
}

Debugging Order

调试顺序

When cache behavior is wrong — stale data, no invalidation, unexpected freshness:
  1. Is
    "use cache"
    the first statement in the function body, before any
    await
    ?
  2. Is a dynamic API (
    cookies
    ,
    headers
    ,
    auth
    ) called inside a cached function?
  3. Does the collection tag in
    cacheTag()
    exactly match the tag string in the registry?
  4. If surgical invalidation is needed, does the entity tag factory exist in
    lib/cache/tags.ts
    ?
  5. Is the revalidation utility actually called after the mutation completes?
  6. Are Suspense boundaries correctly isolating dynamic from cached sections?
  7. Run
    next build
    and inspect static vs dynamic route output.

当缓存行为异常时 — 数据过期、未失效、新鲜度不符合预期:
  1. "use cache"
    是否是函数体中的第一个语句,在所有
    await
    之前?
  2. 是否在缓存函数中调用了动态API(
    cookies
    headers
    auth
    )?
  3. cacheTag()
    中的集合标签是否与注册表中的标签字符串完全匹配?
  4. 如果需要精确失效,
    lib/cache/tags.ts
    中是否存在实体标签工厂?
  5. 变更操作完成后是否实际调用了重新验证工具?
  6. Suspense边界是否正确隔离了动态内容与缓存内容?
  7. 运行
    next build
    并检查静态与动态路由的输出。

Post-Implementation Checklist

实施后检查清单

  • next.config.ts
    has
    cacheComponents: true
  • lib/cache/tags.ts
    has a collection tag for every data domain
  • Entity tag factories added only where mutations require surgical
    updateTag()
  • lib/cache/revalidate.ts
    contains all
    updateTag()
    calls — nowhere else
  • Every
    "use cache"
    function has both
    cacheTag()
    and
    cacheLife()
  • "use cache"
    is the first statement in every function that uses it
  • No dynamic request APIs (
    cookies
    ,
    headers
    ,
    auth
    ) inside cached functions
  • Closure variables are primitives — not whole objects or class instances
  • Page components are synchronous and do not fetch data
  • SuspenseOnSearchParams
    used on every page with search or filter params
  • Mutations call revalidation utilities — never
    updateTag()
    directly
  • next build
    confirms expected static/dynamic rendering boundaries

  • next.config.ts
    中已设置
    cacheComponents: true
  • lib/cache/tags.ts
    为每个数据领域都配置了集合标签
  • 仅在变更操作需要精确
    updateTag()
    时添加了实体标签工厂
  • 所有
    updateTag()
    调用都在
    lib/cache/revalidate.ts
    中 — 无其他地方调用
  • 每个
    "use cache"
    函数都同时包含
    cacheTag()
    cacheLife()
  • "use cache"
    是每个使用该函数的第一个语句
  • 缓存函数中未调用动态请求API(
    cookies
    headers
    auth
  • 闭包变量是基础类型 — 不是整个对象或类实例
  • 页面组件是同步的,不进行数据获取
  • 所有带搜索或过滤参数的页面都使用了
    SuspenseOnSearchParams
  • 变更操作调用重新验证工具 — 绝不直接调用
    updateTag()
  • next build
    确认了预期的静态/动态渲染边界

Rules Summary

规则总结

Always

始终遵守

  • Centralize tag strings in
    lib/cache/tags.ts
  • Centralize
    updateTag()
    calls in
    lib/cache/revalidate.ts
  • Add a collection tag to every
    "use cache"
    function
  • Put
    "use cache"
    first — before any
    await
  • Include both
    cacheTag()
    and
    cacheLife()
    in every cached function
  • Read
    cookies()
    /
    headers()
    outside cached functions, pass results as props
  • Use
    SuspenseOnSearchParams
    on any page with search or filter params
  • Run
    next build
    to verify boundaries
  • lib/cache/tags.ts
    中集中管理标签字符串
  • lib/cache/revalidate.ts
    中集中管理
    updateTag()
    调用
  • 为每个
    "use cache"
    函数添加集合标签
  • "use cache"
    放在首位 — 在任何
    await
    之前
  • 每个缓存函数都包含
    cacheTag()
    cacheLife()
  • 在缓存函数之外读取
    cookies()
    /
    headers()
    ,将结果作为props传递
  • 在任何带搜索或过滤参数的页面中使用
    SuspenseOnSearchParams
  • 运行
    next build
    验证边界

Never

绝对禁止

  • Write raw tag strings outside
    lib/cache/tags.ts
  • Call
    updateTag()
    directly in server actions, route handlers, or components
  • Add entity tag factories without a confirmed mutation that requires them
  • Call dynamic request APIs inside
    "use cache"
    functions
  • Capture large objects or non-serializable values in closures
  • Fetch data inside
    page.tsx
  • Place
    "use cache"
    after an
    await
    — it will be ignored
  • lib/cache/tags.ts
    之外编写原始标签字符串
  • 在服务器操作、路由处理程序或组件中直接调用
    updateTag()
  • 在未确认需要的情况下添加实体标签工厂
  • "use cache"
    函数中调用动态请求API
  • 在闭包中捕获大对象或不可序列化的值
  • page.tsx
    中获取数据
  • "use cache"
    放在
    await
    之后 — 会被忽略