关于 cache-components
cache-components 是面向启用 Cache Components 的 Next.js 项目的开发技能,介绍如何使用缓存 API 和 Suspense 边界组合静态、缓存及动态内容。
主要功能
- 01检测 Cache Components 配置
- 02指导使用 'use cache' 指令
- 03缓存时长与标签失效 API
- 04静态外壳和动态流式内容
- 05构建时检查与代码审查指引
适用场景
- 01为服务端数据获取添加缓存
- 02组合缓存内容与动态内容构建页面
- 03在数据变更后使缓存失效
- 04审查启用 Cache Components 的代码库
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zoonk/ zoonk --skill cache-components安装后在 Claude Code 中输入/cache-components确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
PATTERNS.mdREFERENCE.mdTROUBLESHOOTING.md详细介绍
Next.js 缓存组件
自动激活:在
next.config中设置cacheComponents: true的项目会自动激活此技能。
项目检测
开始在 Next.js 项目中工作时,请检查是否启用了缓存组件:
# Check next.config.ts or next.config.js for cacheComponents
grep -r "cacheComponents" next.config.* 2>/dev/null
如果发现 cacheComponents: true,在以下场景中主动应用此技能的模式:
- 编写 React 服务器组件
- 实现数据获取
- 创建包含变更操作的服务器操作
- 优化页面性能
- 检查现有组件代码
缓存组件支持部分预渲染(PPR)——将静态 HTML 外壳与动态流式内容相结合,以实现最佳性能。
理念:代码优于配置
缓存组件代表着从分段配置转向组合式代码:
| 之前(已弃用) | 之后(缓存组件) |
|---|---|
export const revalidate = 3600 | 在 'use cache' 内使用 cacheLife('hours') |
export const dynamic = 'force-static' | 使用 'use cache' 和 Suspense 边界 |
| 非此即彼的静态/动态 | 细粒度:静态外壳 + 缓存内容 + 动态内容 |
核心原则:组件将缓存逻辑与自身放在一起,而不仅仅是将缓存逻辑与数据放在一起。Next.js 会在构建时提供反馈,引导你采用最佳模式。
核心概念
┌─────────────────────────────────────────────────────┐
│ Static Shell │
│ (Sent immediately to browser) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Header │ │ Cached │ │ Suspense │ │
│ │ (static) │ │ Content │ │ Fallback │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Dynamic │ │
│ │ (streams) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
心智模型:缓存决策树
编写 React 服务器组件时,请依次思考以下问题:
┌─────────────────────────────────────────────────────────┐
│ Does this component fetch data or perform I/O? │
└─────────────────────┬───────────────────────────────────┘
│
┌──────────▼──────────┐
│ YES │ NO → Pure component, no action needed
└──────────┬──────────┘
│
┌─────────────────▼─────────────────┐
│ Does it depend on request context? │
│ (cookies, headers, searchParams) │
└─────────────────┬─────────────────┘
│
┌────────────┴────────────┐
│ │
┌────▼────┐ ┌─────▼─────┐
│ YES │ │ NO │
└────┬────┘ └─────┬─────┘
│ │
│ ┌─────▼─────────────────┐
│ │ Can this be cached? │
│ │ (same for all users?) │
│ └─────┬─────────────────┘
│ │
│ ┌──────────┴──────────┐
│ │ │
│ ┌────▼────┐ ┌─────▼─────┐
│ │ YES │ │ NO │
│ └────┬────┘ └─────┬─────┘
│ │ │
│ ▼ │
│ 'use cache' │
│ + cacheTag() │
│ │
└──────────────┬─────────────────────┘
│
▼
Wrap in <Suspense>
(dynamic streaming)
关键要点:'use cache' 指令适用于_所有用户都相同_的数据。用户专属数据应通过 Suspense 保持为动态内容。
快速入门
启用缓存组件
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
基本用法
// Cached component - output included in static shell
async function CachedPosts() {
"use cache";
const posts = await db.posts.findMany();
return <PostList posts={posts} />;
}
// Page with static + cached + dynamic content
export default async function BlogPage() {
return (
<>
<Header /> {/* Static */}
<CachedPosts /> {/* Cached */}
<Suspense fallback={<Skeleton />}>
<DynamicComments /> {/* Dynamic - streams */}
</Suspense>
</>
);
}
核心 API
1. 'use cache' 指令
将代码标记为可缓存。可应用于三个层级:
// File-level: All exports are cached
"use cache";
export async function getData() {
/* ... */
}
export async function Component() {
/* ... */
}
// Component-level
async function UserCard({ id }: { id: string }) {
"use cache";
const user = await fetchUser(id);
return <Card>{user.name}</Card>;
}
// Function-level
async function fetchWithCache(url: string) {
"use cache";
return fetch(url).then((r) => r.json());
}
重要:所有缓存函数都必须是 async。
2. cacheLife() — 控制缓存时长
import { cacheLife } from "next/cache";
async function Posts() {
"use cache";
cacheLife("hours"); // Use a predefined profile
// Or custom configuration:
cacheLife({
stale: 60, // 1 min - client cache validity
revalidate: 3600, // 1 hr - start background refresh
expire: 86400, // 1 day - absolute expiration
});
return await db.posts.findMany();
}
预定义配置:'default'、'seconds'、'minutes'、'hours'、'days'、'weeks'、'max'
3. cacheTag() — 为缓存添加失效标签
import { cacheTag } from "next/cache";
async function BlogPosts() {
"use cache";
cacheTag("posts");
cacheLife("days");
return await db.posts.findMany();
}
async function UserProfile({ userId }: { userId: string }) {
"use cache";
cacheTag("users", `user-${userId}`); // Multiple tags
return await db.users.findUnique({ where: { id: userId } });
}
4. updateTag() — 立即使缓存失效
用于实现读取自己的写入结果语义:
"use server";
import { updateTag } from "next/cache";
export async function createPost(formData: FormData) {
await db.posts.create({ data: formData });
updateTag("posts"); // Client immediately sees fresh data
}
5. revalidateTag() — 在后台重新验证
用于实现过期后重新验证模式:
"use server";
import { revalidateTag } from "next/cache";
export async function updatePost(id: string, data: FormData) {
await db.posts.update({ where: { id }, data });
revalidateTag("posts", "max"); // Serve stale, refresh in background
}
各模式的适用场景
| 内容类型 | API | 行为 |
|---|---|---|
| 静态 | 无指令 | 在构建时渲染 |
| 缓存 | 'use cache' | 包含在静态外壳中,并重新验证 |
| 动态 | 在 <Suspense> 内 | 在请求时流式传输 |
参数排列组合与子外壳
重要概念:启用缓存组件后,Next.js 会渲染所提供参数的所有排列组合,以创建可复用的子外壳。
// app/products/[category]/[slug]/page.tsx
export async function generateStaticParams() {
return [
{ category: "jackets", slug: "classic-bomber" },
{ category: "jackets", slug: "essential-windbreaker" },
{ category: "accessories", slug: "thermal-fleece-gloves" },
];
}
Next.js 会渲染以下路由:
/products/jackets/classic-bomber ← Full params (complete page)
/products/jackets/essential-windbreaker ← Full params (complete page)
/products/accessories/thermal-fleece-gloves ← Full params (complete page)
/products/jackets/[slug] ← Partial params (category subshell)
/products/accessories/[slug] ← Partial params (category subshell)
/products/[category]/[slug] ← No params (fallback shell)
重要性:类别子外壳(/products/jackets/[slug])可供任何夹克产品复用,包括未包含在 generateStaticParams 中的产品。用户访问未列出的夹克时,会立即看到缓存的类别外壳,产品详情则会以流式方式加载。
generateStaticParams 要求
启用缓存组件后:
- 必须至少提供一个参数——空数组现在会导致构建错误(防止生产环境中出现静默故障)
- 参数可证明静态安全性——提供参数后,Next.js 可以验证没有调用动态 API
- 部分参数会创建子外壳——每种唯一的排列组合都会生成可复用的外壳
// ❌ ERROR with Cache Components
export function generateStaticParams() {
return []; // Build error: must provide at least one param
}
// ✅ CORRECT: Provide real params
export async function generateStaticParams() {
const products = await getPopularProducts();
return products.map(({ category, slug }) => ({ category, slug }));
}
缓存键 = 参数
参数会成为缓存键的一部分:
// Different userId = different cache entry
async function UserData({ userId }: { userId: string }) {
"use cache";
cacheTag(`user-${userId}`);
return await fetchUser(userId);
}
构建时反馈
缓存组件会在开发期间提供早期反馈。这些构建错误会引导你采用最佳模式:
错误:Suspense 外部存在动态数据
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
解决方案:使用 <Suspense> 包裹动态组件:
<Suspense fallback={<Skeleton />}>
<ComponentThatUsesCookies />
</Suspense>
错误:Suspense 外存在未缓存的数据
Error: Accessing uncached data outside Suspense
解决方案:缓存数据或使用 Suspense 包裹:
// Option 1: Cache it
async function ProductData({ id }: { id: string }) {
"use cache";
return await db.products.findUnique({ where: { id } });
}
// Option 2: Make it dynamic with Suspense
<Suspense fallback={<Loading />}>
<DynamicProductData id={id} />
</Suspense>;
错误:在缓存内请求数据
Error: Cannot access cookies/headers inside 'use cache'
解决方案:将运行时数据提取到缓存边界之外(参见上文“处理运行时数据”)。
其他资源
- 完整的 API 参考请参阅 REFERENCE.md
- 常见模式和示例请参阅 PATTERNS.md
- 调试和故障排除请参阅 TROUBLESHOOTING.md
代码生成指南
生成 Cache Component 代码时:
- 始终使用
async- 所有缓存函数都必须是异步函数 - 将
'use cache'放在首位 - 必须是函数体中的第一条语句 - 尽早调用
cacheLife()- 应紧跟在'use cache'指令之后 - 使用有意义的标签 - 使用与失效需求相符的语义化标签
- 提取运行时数据 - 将
cookies()/headers()移到缓存作用域之外 - 包裹动态内容 - 对未缓存的异步组件使用
<Suspense>
主动应用(启用 Cache Components 时)
检测到项目中存在 cacheComponents: true 时,自动应用这些模式:
编写数据获取组件时
问问自己:“这些数据可以缓存吗?”如果可以,就添加 'use cache':
// Before: Uncached fetch
async function ProductList() {
const products = await db.products.findMany();
return <Grid products={products} />;
}
// After: With caching
async function ProductList() {
"use cache";
cacheTag("products");
cacheLife("hours");
const products = await db.products.findMany();
return <Grid products={products} />;
}
编写 Server Actions 时
执行变更后,始终使相关缓存失效:
"use server";
import { updateTag } from "next/cache";
export async function createProduct(data: FormData) {
await db.products.create({ data });
updateTag("products"); // Don't forget!
}
组合页面时
采用静态外壳 + 缓存内容 + 动态流式传输的结构:
export default async function Page() {
return (
<>
<StaticHeader /> {/* No cache needed */}
<CachedContent /> {/* 'use cache' */}
<Suspense fallback={<Skeleton />}>
<DynamicUserContent /> {/* Streams at runtime */}
</Suspense>
</>
);
}
审查代码时
在 Cache Components 项目中标记以下问题:
- 在适合缓存的情况下,获取数据时未使用
'use cache' - 缺少
cacheTag()调用(导致无法使缓存失效) - Server Actions 在变更后未调用
updateTag()/revalidateTag() - 在
'use cache'作用域内调用cookies()/headers() - 动态组件未使用
<Suspense>边界 - 已弃用:
export const revalidate- 替换为在'use cache'中使用cacheLife() - 已弃用:
export const dynamic- 替换为 Suspense + 缓存边界 -
generateStaticParams()返回值为空 - 必须至少提供一个参数
注意
本项目默认将 cacheLife 设置为 max 配置,因此大多数情况下无需调用 cacheLife(),除非需要自定义缓存时长。