Next.js Cache Components
Next.js 16 的 Cache Components 模型通过 cacheComponents 与 use cache 显式标记可复用的数据或 UI 结果,并允许同一页面组合静态、缓存与请求时动态区域。
[!info] related notes
- 所属 MOC:
- 前置概念: nextjs-server-component-data-fetching
- 并列概念:
- 易混淆概念:
- 关系笔记: nextjs-cache-vs-tanstack-query、cache-strategies-and-consistency
Next.js Cache Components
一句话定义
Next.js 16 的 Cache Components 模型把“缓存”从隐式默认行为变成更明确的边界:开启 cacheComponents 后,数据获取默认可以保持请求时动态;只有需要复用的函数、组件、Page 或 Layout 才通过 "use cache" 显式进入缓存作用域。
fresh-by-default runtime work
+
explicit "use cache" islands
+
Suspense / Streaming dynamic holes
基本配置
// next.config.ts
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
然后在需要缓存的异步边界里:
export async function getPosts() {
"use cache"
return postRepository.list()
}
或者直接缓存组件结果:
export default async function Page() {
"use cache"
const posts = await getPosts()
return <PostList posts={posts} />
}
可以缓存什么
"use cache" 可以用于不同粒度:
Data function
→ 缓存函数结果
Server Component
→ 缓存组件输出
Page / Layout
→ 缓存更大的 UI 子树
所以它不是“Fetch API 的一个缓存参数”,而是一个 server-side computation / rendering cache boundary。
Data-level 与 UI-level Cache
Data-level
async function getProducts() {
"use cache"
return productApi.list()
}
适合:
- 多个组件共享同一昂贵数据结果
- 不想把整个页面都绑定到同一缓存生命周期
- 希望缓存边界靠近真正的数据读取
UI-level
async function ProductSection() {
"use cache"
const products = await getProducts()
return <ProductGrid products={products} />
}
适合:
- 整个 UI 子树都由同一组相对稳定数据决定
- 想直接复用计算/渲染结果
同一个页面可以混合三种区域
Cache Components 的重要价值不是把页面简单分成“Static 或 Dynamic”,而是允许:
Page
├── Static shell
├── Cached content
└── Fresh runtime content
↓
Suspense / Streaming
例如:
Blog Page
├── Header static
├── Article List cached
└── User Notification request-time fresh
这使缓存边界可以贴近真实业务需求,而不是整个 Route 一刀切。
不缓存也是明确选择
如果一份数据要求:
每次请求都应该拿当前状态
例如:
- 正在运行的 Agent Run 状态
- 高频变化的 seq/events
- 强 freshness 的用户权限
- 实时 Consultation 状态
就不要为了“Next.js 有 Cache”而强行 "use cache"。
<Suspense fallback={<RunSkeleton />}>
<CurrentRun />
</Suspense>
更合适的模型是:
fresh request-time read
+
Suspense / Streaming
+
SSE / WebSocket live updates
Cache Key 从哪里来
缓存必须有身份。对于:
async function getUser(id: string) {
"use cache"
return userApi.get(id)
}
id 等参数会参与缓存身份,因此:
getUser("123")
≠
getUser("456")
这和 TanStack Query 的 queryKey 在思想上类似:缓存不能是没有身份的匿名副本。
但不要把两者视为同一个机制,见 Next.js Cache 与 TanStack Query 的关系。
Runtime Data 的边界
诸如:
cookies()
headers()
searchParams
属于请求到来后才知道的 runtime data。一个稳定做法是:
先读取 runtime value
↓
作为明确参数传入 cached function
↓
参数进入 cache identity
例如:
async function ProfilePage() {
const sessionId = (await cookies()).get("session")?.value
return <CachedProfile sessionId={sessionId ?? ""} />
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
"use cache"
return getProfile(sessionId)
}
真正涉及私人 runtime data 的场景还存在 "use cache: private" 等能力,但应先保证身份、授权和隐私边界正确,再谈性能。
缓存不是 Truth
"use cache" 只是允许复用服务器结果,不会让缓存成为新的 authoritative owner:
Owning Service / DB
↓ authoritative truth
Next.js Cache
↓ server-side projection
Browser Cache / Query Cache
↓ client projection
一旦业务事实变化,缓存需要通过时间或事件重新验证。见 Next.js cacheLife 与 Next.js Tag-based Revalidation。
BodySense 边界
对 BodySense:
Assessment Result
→ 可否缓存要谨慎评估
→ user / assessment identity 必须明确
→ Go Backend 仍然是业务 owner
Active Agent Run
→ 不适合长缓存
→ fresh snapshot + SSE
即使缓存发生在 Next.js Server 层,也不能绕过 数据所有权与服务边界。
常见误区
“用了 Cache Components,所有 Server Component 都自动长期缓存”
错。当前模型强调显式缓存边界;不需要复用的 runtime work 可以保持动态。
“use cache = HTTP Cache-Control”
错。它是 Next.js 的应用/渲染缓存机制,不应直接等同浏览器 HTTP Cache-Control。
“缓存得越多越快,因此越好”
错。缓存同时引入 stale、身份、失效、跨用户隔离、观测和多层一致性问题。
最短记忆
cacheComponents
→ 开启新的显式缓存模型
"use cache"
→ 这个函数 / 组件 / 页面结果可以复用
没有 "use cache"
→ 可以保持 request-time fresh
Cache
≠ Truth