nextjs-cache-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHow 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 (, , etc.) with names from their codebase before writing any code.
[Entity][collection]text
$ARGUMENTS阅读下方用户输入,然后将本文件中的所有规则和模板应用到他们的实际项目中。
在编写任何代码之前,将所有占位符(、等)替换为他们代码库中的名称。
[Entity][collection]text
$ARGUMENTSMental 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 function automatically.
You never construct cache keys manually.
"use cache"| Component | What it includes |
|---|---|
| Build ID | Changes on every deploy — all caches invalidated automatically |
| Function ID | Hash of the function's file path and position in source |
| Arguments | Every value passed to the function at call time |
| Closure variables | Every 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" 放置规则
| Placement | When to use |
|---|---|
| Top of an async function body | Single data-fetching function |
| Top of an async Server Component body | Entire component output is cacheable |
| Never in a page component | Page components orchestrate; they do not fetch |
"use cache"awaitts
// 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"awaitts
// 正确写法
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: , , , plain objects, arrays.
stringnumberbooleantsx
// 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.
闭包变量应保持为可序列化的基础类型:、、、普通对象、数组。
stringnumberbooleantsx
// 错误 — 大对象被序列化到缓存键中
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.tsSingle 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 format
domain:id - Match your actual data model — do not invent names
- Do not add entity factories speculatively — only when a mutation requires on that entry
updateTag()
- 仅使用小写
- 实体标签使用格式
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.tsAll calls live here. Mutations import these functions — they never call directly.
updateTag()updateTag()文件:
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 in data-fetching functions. Never fetch inside page components.
"use cache"将放在数据获取函数中。绝不在页面组件中进行数据获取。
"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 does not re-trigger its fallback on client-side navigation when only changes.
Use this wrapper on every page with search or filter params.
<Suspense>searchParamstsx
// 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>在仅变更的客户端导航中不会重新触发fallback。
在所有带搜索或过滤参数的页面中使用此包装器。
searchParamstsx
// 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 , , or inside a function.
Read them outside the cache boundary and pass derived primitives as props.
cookies()headers()auth()"use cache"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 />;
}绝不在函数中调用、或。
在缓存边界之外读取这些内容,并将派生的基础类型作为props传递。
"use cache"cookies()headers()auth()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 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 directly.
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
// 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
缓存时长参考
| Profile | Use when |
|---|---|
| Near-real-time data (live feeds, counters) |
| Frequently updated content (dashboards, notifications) |
| Moderately stable content (listings, articles, configs) |
| Rarely updated content (reference data, documentation) |
| 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
});| 配置 | 使用场景 |
|---|---|
| 近实时数据(实时信息流、计数器) |
| 频繁更新的内容(仪表盘、通知) |
| 中度稳定的内容(列表、文章、配置) |
| 极少更新的内容(参考数据、文档) |
| 近乎永久有效(构建时常量、静态资源) |
如需细粒度控制:
ts
cacheLife({
stale: 3600, // 最多1小时内返回过期内容
revalidate: 7200, // 每2小时进行一次后台重新验证
expire: 86400, // 1天后强制过期
});Limitations
限制条件
- Edge runtime is not supported — requires Node.js
- Static export () is not supported
output: "export" - and
Math.random()insideDate.now()execute once at build time, not per request"use cache"
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:
- Is the first statement in the function body, before any
"use cache"?await - Is a dynamic API (,
cookies,headers) called inside a cached function?auth - Does the collection tag in exactly match the tag string in the registry?
cacheTag() - If surgical invalidation is needed, does the entity tag factory exist in ?
lib/cache/tags.ts - Is the revalidation utility actually called after the mutation completes?
- Are Suspense boundaries correctly isolating dynamic from cached sections?
- Run and inspect static vs dynamic route output.
next build
当缓存行为异常时 — 数据过期、未失效、新鲜度不符合预期:
- 是否是函数体中的第一个语句,在所有
"use cache"之前?await - 是否在缓存函数中调用了动态API(、
cookies、headers)?auth - 中的集合标签是否与注册表中的标签字符串完全匹配?
cacheTag() - 如果需要精确失效,中是否存在实体标签工厂?
lib/cache/tags.ts - 变更操作完成后是否实际调用了重新验证工具?
- Suspense边界是否正确隔离了动态内容与缓存内容?
- 运行并检查静态与动态路由的输出。
next build
Post-Implementation Checklist
实施后检查清单
- has
next.config.tscacheComponents: true - has a collection tag for every data domain
lib/cache/tags.ts - Entity tag factories added only where mutations require surgical
updateTag() - contains all
lib/cache/revalidate.tscalls — nowhere elseupdateTag() - Every function has both
"use cache"andcacheTag()cacheLife() - is the first statement in every function that uses it
"use cache" - No dynamic request APIs (,
cookies,headers) inside cached functionsauth - Closure variables are primitives — not whole objects or class instances
- Page components are synchronous and do not fetch data
- used on every page with search or filter params
SuspenseOnSearchParams - Mutations call revalidation utilities — never directly
updateTag() - confirms expected static/dynamic rendering boundaries
next build
- 中已设置
next.config.tscacheComponents: 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 calls in
updateTag()lib/cache/revalidate.ts - Add a collection tag to every function
"use cache" - Put first — before any
"use cache"await - Include both and
cacheTag()in every cached functioncacheLife() - Read /
cookies()outside cached functions, pass results as propsheaders() - Use on any page with search or filter params
SuspenseOnSearchParams - Run to verify boundaries
next build
- 在中集中管理标签字符串
lib/cache/tags.ts - 在中集中管理
lib/cache/revalidate.ts调用updateTag() - 为每个函数添加集合标签
"use cache" - 将放在首位 — 在任何
"use cache"之前await - 每个缓存函数都包含和
cacheTag()cacheLife() - 在缓存函数之外读取/
cookies(),将结果作为props传递headers() - 在任何带搜索或过滤参数的页面中使用
SuspenseOnSearchParams - 运行验证边界
next build
Never
绝对禁止
- Write raw tag strings outside
lib/cache/tags.ts - Call directly in server actions, route handlers, or components
updateTag() - Add entity tag factories without a confirmed mutation that requires them
- Call dynamic request APIs inside functions
"use cache" - Capture large objects or non-serializable values in closures
- Fetch data inside
page.tsx - Place after an
"use cache"— it will be ignoredawait
- 在之外编写原始标签字符串
lib/cache/tags.ts - 在服务器操作、路由处理程序或组件中直接调用
updateTag() - 在未确认需要的情况下添加实体标签工厂
- 在函数中调用动态请求API
"use cache" - 在闭包中捕获大对象或不可序列化的值
- 在中获取数据
page.tsx - 将放在
"use cache"之后 — 会被忽略await