mcpField
C
cache-components

cache-components

github.com/zoonk开发15839 次安装3 周前更新
代码仓库

关于 cache-components

cache-components 是面向启用 Cache Components 的 Next.js 项目的开发技能,介绍如何使用缓存 API 和 Suspense 边界组合静态、缓存及动态内容。

主要功能

  1. 01检测 Cache Components 配置
  2. 02指导使用 'use cache' 指令
  3. 03缓存时长与标签失效 API
  4. 04静态外壳和动态流式内容
  5. 05构建时检查与代码审查指引

适用场景

  1. 01为服务端数据获取添加缓存
  2. 02组合缓存内容与动态内容构建页面
  3. 03在数据变更后使缓存失效
  4. 04审查启用 Cache Components 的代码库

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zoonk/zoonk --skill cache-components

安装后在 Claude Code 中输入/cache-components确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

PATTERNS.md
REFERENCE.md
TROUBLESHOOTING.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 要求

启用缓存组件后:

  1. 必须至少提供一个参数——空数组现在会导致构建错误(防止生产环境中出现静默故障)
  2. 参数可证明静态安全性——提供参数后,Next.js 可以验证没有调用动态 API
  3. 部分参数会创建子外壳——每种唯一的排列组合都会生成可复用的外壳
// ❌ 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'

解决方案:将运行时数据提取到缓存边界之外(参见上文“处理运行时数据”)。

其他资源

代码生成指南

生成 Cache Component 代码时:

  1. 始终使用 async - 所有缓存函数都必须是异步函数
  2. 将 'use cache' 放在首位 - 必须是函数体中的第一条语句
  3. 尽早调用 cacheLife() - 应紧跟在 'use cache' 指令之后
  4. 使用有意义的标签 - 使用与失效需求相符的语义化标签
  5. 提取运行时数据 - 将 cookies()/headers() 移到缓存作用域之外
  6. 包裹动态内容 - 对未缓存的异步组件使用 <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(),除非需要自定义缓存时长。

评论

支持 Markdown