cache-components:Next.js 缓存架构的一次范式转移

缓存这件事,在 Next.js 里一直是玄学。revalidatedynamicfetchCache 这些配置项互相影响,很多项目最后是靠”试出来的组合”在跑,没人说得清为什么这么配。Vercel 官方在 Smithery 上发布的 cache-components 技能,就是为了终结这种状态。

这个技能的全称是 Cache Components and Partial Prerendering,专注指导 Agent 在 Next.js 项目里落地静态壳加动态流的混合渲染。它在 Smithery 上有 13.7 万次浏览,安装量却只有 22 次。这个反差本身就是信号:围观的人多,真正动手的人少,而动手的人大概率会领先一个身位。

cache-components:Next.js 缓存架构的一次范式转移

我说”范式转移”,是因为它动的是 Next.js 最顽固的地基。过去十年,从 Pages Router 到 App Router,缓存的表达方式一直在配置项里打转,没有一个版本真正解决”同一个页面里既有静态内容又有动态内容”的根本矛盾。Cache Components 换了个思路:不再问”这个页面是什么类型”,而是问”这个组件的数据该活多久”。

这篇文章会带你走一遍:

  • 这个技能到底解决什么问题
  • 核心 API 长什么样
  • 三层内容模型怎么运转
  • 它凭什么说是配置项时代的终结者

读完你能判断自己的项目该不该上车,也能在 Agent 写缓存代码时看出它有没有按官方套路来。

先说我的结论:这不是一个教你怎么配缓存的技能,而是一个把 Vercel 对缓存的最佳实践编码进 Agent 行为模式的技能。它想改变的不是某个配置值,而是 Next.js 开发者思考缓存的整个方式。

这种”编码进 Agent”的思路值得单独拎出来说。Vercel 没有选择写一篇博客让你自己领会,而是把决策树、API 用法、迁移路径全部塞进一个可以被任何 AI 编程工具加载的技能文件。你装好它,相当于雇了一个带着 Vercel 官方缓存团队大脑的结对程序员。

环境准备

技能要求 Next.js 项目开启 cacheComponents: true。这是它的自动激活条件,Agent 检测到项目里存在这个配置才会主动介入。安装方式很轻,一行命令:

npx skills add https://github.com/smithery/ai --skill cache-components

装完之后,技能并不会常驻你的对话。它的触发逻辑是项目检测:Agent 在 next.config.* 里 grep cacheComponents,找到了就启用,找不到就当作普通 Next.js 项目处理。这种”按项目特征自动激活”的设计,是 Vercel 技能家族的统一风格。

要注意一个版本前提。这个技能针对的是 Next.js 16 系列,Cache Components 相关的完整 API,包括 use cache 指令和 cacheLife,都是 16.3 之后才齐的。老项目想用,得先跑 npx @next/codemod@latest upgrade latest 升级。

升级过程还有一个容易忽视的坑:experimental.dynamicIO 这个旧配置必须移除,它在新版本里被重命名成了顶层的 cacheComponents,保留旧 key 会在构建开始前直接报错中止。experimental.useCache 则成了废弃别名,留着不报错,但会和新的开关语义混淆,建议一起清理干净。

操作流程

技能的核心工作流,是一棵”缓存决策树”。Agent 在写任何 React Server Component 时都会先过一遍:组件要不要取数,取数依不依赖请求上下文,数据是不是所有用户共享。这棵树的答案直接决定代码往哪个方向走:

cache-components:Next.js 缓存架构的一次范式转移

顺着这棵树走到底,你会发现一个反直觉的结论:use cache 指令不是给”个性化内容”用的,恰恰相反,它是给”所有用户看到同一份数据”用的。用户相关的数据,比如带 cookie 的会话信息,反而要留在动态流里。

cacheLife 是控制缓存时长的入口。它有七个预设档位:

default / seconds / minutes / hours / days / weeks / max

大多数场景选档位就够了。需要精细控制时也能传入自定义对象,分别指定 stalerevalidateexpire 三个时间值。这比手写 revalidate = 3600 多了一层语义:stale 管客户端缓存有效期,revalidate 管后台刷新时机,expire 管绝对过期时间。

实际落地的时候,三层结构是骨架。静态壳负责立即渲染的布局框架,缓存层放那些”所有用户都一样但会过期”的内容,动态流处理真正逐请求变化的数据。三者靠 Suspense 边界切开,互不阻塞:

// 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>
    </>
  )
}

这里最容易踩的坑是 'use cache' 的放置层级。它能用在文件级、组件级、函数级,但有一个硬约束:被缓存的函数必须是 async。放错层级会导致整棵子树被错误地排除在静态壳外,构建产物里出现一堆意外的 Dynamic 标记。

失效机制是三件套协同:cacheTag 打标签,updateTag 立即失效,revalidateTag 后台刷新。一个发帖场景的完整写法长这样:帖子创建成功后调用 updateTag('posts'),客户端立刻读到新数据,实现 read-your-own-writes;而其他用户的读取走 revalidateTag,先返回旧数据再后台重建,避免读操作被写操作拖慢。

关键设计

cache-components 的底层是一套完整的内容分类模型。它把页面内容分成三种:静态壳、缓存内容、动态内容。静态壳构建时预渲染,缓存内容复用上一次的结果并后台刷新,动态内容每次请求实时流式输出。三种内容同页共存,互不阻塞:

cache-components:Next.js 缓存架构的一次范式转移

这个设计最激进的地方,是把决策从”页面级配置”下沉到了”组件级代码”。旧时代的 Next.js 用 export const revalidate = 3600 这种路由级配置控制缓存,一个页面只能有一种缓存策略。现在缓存边界和组件绑定在一起,同一页面里不同区块可以拥有完全不同的生命周期。

看两组代码的对照就明白了。旧写法是声明式配置,新写法是组合式代码,缓存边界从”路由”下沉到了”组件”:

// Before: 声明式配置,整个页面一种策略
export const revalidate = 3600
export const dynamic = 'force-static'

// After: 组合式代码,每个组件独立决策
async function Posts() {
  'use cache'
  cacheLife('hours')
  return await db.posts.findMany()
}

缓存失效是另一套精心设计的机制。cacheTag 给缓存内容打标签,updateTag 实现写后立即失效,revalidateTag 走后台刷新。这三者的组合解决了一个长期痛点:数据更新后缓存何时失效,过去靠猜测 TTL,现在靠代码精确声明。

再往下挖一层,还有”参数排列与子壳”这个概念。当路由配合 generateStaticParams 时,Cache Components 会为每个参数组合预渲染独立的子壳。这意味着 /products/[id] 这样的路由,每个商品都有自己的静态壳,而不是整个路由共享一个。热度高的商品命中缓存,冷门商品按需生成,互不拖累。

这背后的取舍也很有意思。缓存粒度越细,静态壳的数量越多,构建时间和存储成本随之上升。技能文档里明确建议:只有当页面主体内容对参数不敏感时才值得这么做。它没有无脑推”全部缓存”,而是把决策权留给开发者,这正是它作为一份工程指南而非营销文档的可贵之处。

使用场景

内容分类模型直接映射到使用场景。三种内容用三种方式处理,互不冲突:

内容类型 处理方式 渲染时机
静态 无指令 构建时渲染
缓存 use cache + cacheLife 静态壳内,后台重新验证
动态 <Suspense> 包裹 请求时流式输出

cacheLife 提供了七个预设档位:

default / seconds / minutes / hours / days / weeks / max

对大多数场景,选档位比手写 TTL 更不容易出错。需要精细控制时也能自定义 stalerevalidateexpire 三个值。

参数排列是这套方案容易被低估的细节。用了 generateStaticParams 的路由,Cache Components 会为每个参数组合渲染出独立的可复用子壳,缓存命中率大幅提升。这对商品详情页这类”海量参数、少量热数据”的场景收益最明显。

技能文档里还特别提醒了一个迁移期的反模式:不要把整个页面 body 包进一个高层的 <Suspense>。那样静态壳会被掏空,只剩 <html><body> 的空骨架,构建日志里还显示  标记表示”有预渲染”,实际上用户看到的是先空白后刷出全部内容,体验反而更差。正确做法是把 Suspense 边界下推到真正读取动态数据的位置附近。

洞察与反思

最出乎我意料的一点,是这份技能的克制。它没有鼓励开发者把能缓存的全缓存,反而在决策树里明确留了”不能缓存”的出口。Vercel 对缓存的官方态度从”尽力缓存”变成了”按需缓存”,这个转变比任何 API 都值得注意。

两种心智模型的差异,值得用一张图说清楚。旧模型是”页面一刀切”,新模型是”组件分层流式”:

cache-components:Next.js 缓存架构的一次范式转移

另一个让我反复琢磨的点是 updateTag 与 revalidateTag 的分工。updateTag 是写后同步失效,适合用户提交后立刻要看到自己内容的场景,比如发帖。revalidateTag 是后台异步刷新,先返回旧数据再更新,适合对一致性要求不高的读密集场景。选错方向,要么牺牲实时性,要么牺牲缓存命中率。

当然,这个技能不是银弹。它依赖 Next.js 16.3+ 和 Turbopack,老项目迁移成本不低,revalidate 到 cacheLife 的翻译也不是纯机械替换,需要理解每条配置背后编码的行为。对纯客户端渲染项目,它毫无用武之地。

横向对比同类方案,React Server Components 时代的其他缓存方案大多还停留在”配置项堆叠”。cache-components 的差异化在于把缓存决策变成了可验证的代码模式,Agent 可以照着决策树逐条落地,而不是靠开发者对配置项组合的玄学理解。

还有一层对比容易被忽略:同一个技能在 Vercel 生态里不是孤立存在的。围绕 Cache Components,官方还维护了 next-cache-components-adoption(负责启用开关并清理阻塞路由)和 next-cache-components-optimizer(驱动单条路由达到即时导航)两个配套技能。三者构成完整的迁移链路:先启用,再修阻塞,最后逐路由优化。这暴露了 Vercel 的真实意图:这不是让你手动折腾的功能,而是整套 Agent 驱动的工程化流程。

说回这个技能本身的阅读体验,SKILL.md 写得很克制,没有花哨的架构图,靠的是决策树加代码示例。它对 use cache 的边界描述尤其精准:所有缓存函数必须 async,缓存的数据必须跨用户一致,缓存和组件同址声明。这些约束不是教条,每条背后都有具体的失效场景支撑。

资源地址

资源 链接
Smithery 技能页 https://smithery.ai/skills/vercel/cache-components
Vercel 官方技能目录 https://vercel.com/docs/agent-resources/skills
Next.js Cache Components 迁移指南 https://nextjs.org/docs/app/guides/migrating-to-cache-components
Next.js 源码仓库 https://github.com/vercel/next.js

总结

Cache Components 是 Next.js 缓存架构的分水岭。它把缓存从路由级配置变成了组件级代码,用 use cachecacheLifecacheTag 三件套取代了 revalidate 加 dynamic 的组合拳。这个技能的价值,在于把这一整套范式压缩成 Agent 可以照做的决策流程。

如果你在写新的 Next.js 应用,值得现在就试。开启 cacheComponents: true,把文章列表丢给 use cache,把评论区包进 Suspense,然后观察构建产物里的 Static、Cached、Dynamic 标记如何分布。

要注意的是,这套方案对旧项目不是零成本迁移。文档里明说了 revalidate 和 fetchCache 需要真实翻译,不是删掉就完事;dynamic = 'force-dynamic' 倒是可以直接删除,因为新架构下每条路由默认就是动态的。迁移时逐路由验证,比一次性全量切换稳得多。

这个技能背后的趋势比技能本身更重要。Vercel 在用 Agent Skill 编码自己的最佳实践,让每一个接入的 AI 编程工具都带上官方团队的架构判断。当这种模式成为常态,开发者与框架之间的知识鸿沟,会以另一种方式被填平。

skills资源

Vercel-update-docs:把"文档同步"从口头约定变成工程流程

2026-8-22 9:25:37

AI工具行业动态

Step Image Edit 2 评测:3.5B参数凭什么打赢12B级对手

2026-4-30 12:25:40

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧