Next.js: Cache Components 与 use cache
最后更新:2026-08-26
use cache是 Next.js 16 最具变革性的 API——它把缓存从"数据获取的副作用"升级为"组件级的一等公民"。
1. 你将学到
use cache()指令与 Content Cache 的概念cacheTag()为缓存内容打标签cacheLife()设置缓存过期时间revalidateTag()按需失效缓存- 隐式 fetch 缓存 vs 显式
use cache缓存的区别
2. 一个系统架构师的真实故事
(1) 痛点:同一页面上 4 次相同的 API 调用
Charlie 在审查 TaskFlow Dashboard 时发现:页面上的 <UserAvatar>、<UserGreeting>、<UserStats>、<UserNotifications> 四个组件都在各自调用 fetch('/api/user')——同一个 URL 缓存命中没问题,但数据库查询函数 getUserFromDB() 被调用了 4 次。fetch 缓存只对 HTTP 请求有效,对服务端内部函数调用完全无能为力。
| 问题 | 数据 |
|---|---|
| 页面级数据库调用 | 4 次完全相同的查询 |
| 每次查询耗时 | 200 ms |
| 额外总耗时 | 600 ms 浪费 |
| 数据库 QPS 浪费 | 4x |
(2) use cache 的解法
用
use cache()包裹函数——缓存内部的任何函数调用,包括数据库查询、计算、文件读取。
// app/dashboard/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserData() {
'use cache'
cacheTag('user-data')
cacheLife({ stale: 300, revalidate: 600 })
const user = await db.user.findUnique({ where: { id: 1 } }) // 只执行一次
return user
}
export default async function DashboardPage() {
const user = await getUserData() // 第一次调用执行数据库查询
// 后续调用直接返回缓存结果
return (
<div>
<UserAvatar user={user} />
<UserGreeting user={user} />
<UserStats user={user} />
<UserNotifications user={user} />
</div>
)
}
(3) 收益
| 维度 | 传统模式 | use cache |
|---|---|---|
| 数据库查询次数 | 4 次 | 1 次 |
| 额外延迟 | 600 ms | 0 ms |
| 缓存粒度 | URL 级别 | 函数/组件级别 |
| 自定义逻辑缓存 | ❌ 不支持(仅 HTTP fetch) | ✅ 支持任意代码 |
3. use cache() 指令与 Content Cache
use cache 是一个 函数级指令——在函数体顶部添加 'use cache' 来标记此函数的结果应该被缓存。缓存的内容称为 Content Cache,是 Next.js 16 全新的缓存层,独立于传统的 Data Cache(fetch 缓存)。
graph TB
subgraph "Next.js 16 Cache 体系"
A[Request Memoization<br/>请求级内存]
B[Data Cache<br/>fetch 缓存]
C[Content Cache<br/>use cache]
D[Full Route Cache<br/>全路由缓存]
end
A --> E[同一个请求内<br/>相同 fetch 去重]
B --> F[跨请求持久化<br/>force-cache/no-store]
C --> G[任意函数结果缓存<br/>tag + life 控制]
D --> H[页面级 HTML 缓存]
style C fill:#d4edda
| 缓存层 | 作用域 | 触发器 | 生命周期 |
|---|---|---|---|
| Request Memoization | 单次请求 | 自动(相同 URL) | 请求结束 |
| Data Cache | 跨请求 | fetch(options) |
配置决定 |
| Content Cache | 跨请求 | 'use cache' 指令 |
cacheLife + cacheTag |
| Full Route Cache | 跨请求 | 构建时 / 运行时 | revalidate / on-demand |
(1) 基本语法
// app/cache-demo/actions.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
export async function getExpensiveData(id: string) {
'use cache'
cacheTag('expensive', `id-${id}`) // 打标签
cacheLife({ stale: 60, revalidate: 300 }) // 60 秒内返回 stale,300 秒后重新获取
// 此函数体内的所有操作都会被缓存
const result = await db.query(...)
return result
}
(2) cacheLife 配置
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
stale |
number (秒) | 缓存有效期内直接返回,不触发后台刷新 | { stale: 60 } |
revalidate |
number (秒) | 超过此时间后重新执行函数 | { revalidate: 3600 } |
expire |
number (秒) | 缓存绝对过期,强制重新获取 | { expire: 86400 } |
▶ 示例:基础 use cache 用法(难度⭐)
// app/cache-basic/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getServerTime() {
'use cache'
cacheTag('server-time')
cacheLife({ stale: 10, revalidate: 30 })
// 模拟耗时操作
await new Promise(resolve => setTimeout(resolve, 1000))
return { time: new Date().toISOString(), server: process.env.HOSTNAME ?? 'local' }
}
export default async function CacheBasicPage() {
const [t1, t2, t3] = await Promise.all([
getServerTime(),
getServerTime(),
getServerTime(),
])
return (
<div>
<h1>use cache — Basic</h1>
<p>Call 1: {t1.time}</p>
<p>Call 2: {t2.time}</p>
<p>Call 3: {t3.time}</p>
<p><em>All three calls returned the same cached result (no duplicate computation)</em></p>
</div>
)
}
输出:
Call 1: 2026-07-06T10:00:00.000Z
Call 2: 2026-07-06T10:00:00.000Z ← 相同缓存的毫秒
Call 3: 2026-07-06T10:00:00.000Z
4. cacheTag 与 cacheLife 实战
cacheTag() 为缓存内容打标签,cacheLife() 控制过期策略。结合 revalidateTag() 实现精细化的缓存控制。
(1) 打标签分类策略
graph TB
A[应用数据] --> B[用户数据]
A --> C[产品数据]
A --> D[订单数据]
B --> E[tag: user-profile]
B --> F[tag: user-settings]
C --> G[tag: products]
C --> H[tag: product-{id}]
D --> I[tag: orders]
D --> J[tag: order-{id}]
style A fill:#cce5ff
| 策略 | 标签示例 | 失效操作 | 影响范围 |
|---|---|---|---|
| 细粒度 | product-42 |
revalidateTag('product-42') |
仅单个产品 |
| 中粒度 | products |
revalidateTag('products') |
所有产品 |
| 粗粒度 | catalog |
revalidateTag('catalog') |
整个目录 |
(2) 动态标签
// app/products/[id]/page.tsx
import { unstable_cacheTag as cacheTag } from 'next/cache'
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <ProductView product={product} />
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`) // 动态标签基于参数
return fetch(`https://api.example.com/products/${id}`).then(r => r.json())
}
▶ 示例:分层缓存策略(难度⭐⭐)
// app/cache-strategy/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserProfile(userId: number) {
'use cache'
cacheTag('users', `user-${userId}`)
cacheLife({ stale: 120, revalidate: 600 }) // 2 分钟 stale,10 分钟 revalidate
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(r => r.json())
}
async function getUserPosts(userId: number) {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
export default async function CacheStrategyPage() {
const [profile, posts] = await Promise.all([
getUserProfile(1),
getUserPosts(1),
])
return (
<div>
<h1>{profile.name}</h1>
<p>Email: {profile.email}</p>
<h2>Posts ({posts.length})</h2>
<ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
)
}
▶ 示例:Server Action 中按 tag 失效(难度⭐⭐)
// app/cache-invalidation/page.tsx
import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache'
async function getTaskList() {
'use cache'
cacheTag('tasks')
return fetch('https://jsonplaceholder.typicode.com/todos?_limit=5').then(r => r.json())
}
export default async function CacheInvalidationPage() {
const tasks = await getTaskList()
return (
<div>
<h1>Task List</h1>
<ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
<form action={async () => {
'use server'
revalidateTag('tasks') // 点击按钮立即刷新任务列表缓存
}}>
<button type="submit">Refresh Tasks</button>
</form>
</div>
)
}
5. 隐式 fetch 缓存 vs 显式 use cache 缓存
| 维度 | 隐式 fetch 缓存 (Data Cache) | 显式 use cache (Content Cache) |
|---|---|---|
| 触发方式 | fetch(url) 自动 |
函数体 'use cache' 指令 |
| 缓存内容 | HTTP 响应 | 任意函数返回结果 |
| 适用场景 | API 调用、HTTP 请求 | 数据库查询、复杂计算、文件读取 |
| 标签系统 | next: { tags: [...] } |
cacheTag() 函数 |
| 时间控制 | next: { revalidate: N } |
cacheLife() 函数 |
| 失效方式 | revalidateTag() / revalidatePath() |
revalidateTag()(标签一致) |
(1) 何时用 fetch 缓存,何时用 use cache?
| 场景 | 推荐缓存方式 | 原因 |
|---|---|---|
| 调用外部 API | Data Cache (fetch) |
天然支持 HTTP 语义 |
| 调用数据库 | Content Cache (use cache) |
数据库调用不是 HTTP fetch |
| 复杂计算(50ms+) | Content Cache | 任意函数都可缓存 |
| 读取文件/配置 | Content Cache | 文件操作不可通过 fetch |
| 混合 HTTP + DB 处理 | Content Cache | 整块逻辑打包缓存 |
▶ 示例:两种缓存的组合使用(难度⭐⭐⭐)
// app/cache-combo/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// 1. fetch 缓存:调用外部 API
async function getExternalPosts() {
return fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['external-posts'], revalidate: 300 }
}).then(r => r.json())
}
// 2. use cache:处理后的数据
async function getProcessedPosts() {
'use cache'
cacheTag('processed-posts')
cacheLife({ stale: 60, revalidate: 600 })
const raw = await getExternalPosts() // 依赖 fetch 缓存
return (raw as any[]).map((p: any) => ({
id: p.id,
title: p.title.toUpperCase(),
summary: p.body.slice(0, 100),
}))
}
export default async function CacheComboPage() {
const posts = await getProcessedPosts()
return (
<div>
<h1>Processed Posts ({posts.length})</h1>
<ul>{posts.map((p: any) => (
<li key={p.id}><strong>{p.title}</strong><p>{p.summary}</p></li>
))}</ul>
</div>
)
}
6. 完整示例:用户管理系统的缓存架构
// app/cache-system/page.tsx — 完整的缓存架构
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// ======== 数据层(use cache 显式缓存) ========
type User = { id: number; name: string; email: string }
type Post = { id: number; title: string; body: string }
async function getUsers(): Promise<User[]> {
'use cache'
cacheTag('users')
cacheLife({ stale: 120, revalidate: 600 })
return fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json())
}
async function getUserPosts(userId: number): Promise<Post[]> {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
async function getStats(users: User[]) {
'use cache'
cacheTag('stats')
cacheLife({ stale: 300, revalidate: 1800 })
return {
totalUsers: users.length,
avgNameLength: users.reduce((s, u) => s + u.name.length, 0) / users.length,
}
}
// ======== 页面组件 ========
export default async function CacheSystemPage() {
const users = await getUsers()
const stats = await getStats(users)
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: 24 }}>
<h1>User Management</h1>
<StatsCard stats={stats} />
<div style={{ display: 'grid', gap: 16, marginTop: 24 }}>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
</div>
)
}
// ======== 子组件(独立缓存) ========
async function UserCard({ user }: { user: User }) {
const posts = await getUserPosts(user.id)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{user.name}</h2>
<p style={{ color: '#666' }}>{user.email}</p>
<details>
<summary>Posts ({posts.length})</summary>
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</details>
</div>
)
}
function StatsCard({ stats }: { stats: { totalUsers: number; avgNameLength: number } }) {
return (
<div style={{ background: '#f0f4ff', borderRadius: 8, padding: 16, display: 'flex', gap: 32 }}>
<div><strong>Total Users</strong><p style={{ fontSize: 24 }}>{stats.totalUsers}</p></div>
<div><strong>Avg Name Length</strong><p style={{ fontSize: 24 }}>{stats.avgNameLength.toFixed(1)}</p></div>
</div>
)
}
// ======== 管理操作 ========
// app/cache-system/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshUserData() {
revalidateTag('users') // 刷新用户列表
}
export async function refreshPosts(userId: number) {
revalidateTag(`user-posts-${userId}`) // 刷新特定用户的文章
}
export async function refreshAll() {
revalidateTag('users')
revalidateTag('posts')
revalidateTag('stats')
}
// app/cache-system/admin-button.tsx
'use client'
export function AdminControls() {
return (
<div style={{ display: 'flex', gap: 8, margin: '16px 0' }}>
<form action={async () => {
const { refreshUserData } = await import('./actions')
await refreshUserData()
}}>
<button type="submit">Refresh Users</button>
</form>
<form action={async () => {
const { refreshAll } = await import('./actions')
await refreshAll()
}}>
<button type="submit">Refresh All</button>
</form>
</div>
)
}
▶ 示例:验证缓存命中(难度⭐⭐)
// app/cache-verify/page.tsx — 验证缓存是否命中
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
let callCount = 0
async function getUniqueId() {
'use cache'
cacheTag('unique-id')
cacheLife({ revalidate: 30 })
callCount++
return { id: crypto.randomUUID(), calls: callCount }
}
export default async function CacheVerifyPage() {
const [a, b, c] = await Promise.all([
getUniqueId(),
getUniqueId(),
getUniqueId(),
])
return (
<div>
<h1>Cache Verification</h1>
<p>Result A: {a.id} (call #{a.calls})</p>
<p>Result B: {b.id} (call #{b.calls})</p>
<p>Result C: {c.id} (call #{c.calls})</p>
<p><strong>Same ID + calls = 1 → cache hit</strong></p>
</div>
)
}
输出:
Result A: 550e8400-e29b-41d4-a716-446655440000 (call #1)
Result B: 550e8400-e29b-41d4-a716-446655440000 (call #1) ← 缓存命中
Result C: 550e8400-e29b-41d4-a716-446655440000 (call #1) ← 缓存命中
❓ 常见问题
use cache 和 useMemo 有什么区别?useMemo 是客户端 Hook,只在浏览器中缓存计算、作用域单次渲染。use cache 是服务端指令,跨请求持久化缓存、可带标签和过期时间、支持 Server Action 按需失效。两者完全不同的使用场景。revalidateTag('products') 会同时清除 fetch Data Cache 中带有 next: { tags: ['products'] } 的条目和 Content Cache 中 cacheTag('products') 的条目。这是实现统一缓存失效的关键机制。use cache 可以用于 Client Component 吗?'use cache' 指令只在 Server Component 或服务端函数中有效。客户端组件中应使用 useMemo 或 React Query 等客户端缓存方案。unstable_cacheLife 和 unstable_cacheTag 的 unstable_ 前缀意味着什么?unstable_ 前缀通常会在 1-2 个主版本后移除(预计 Next.js 17 或 18 稳定)。📖 小节
'use cache'指令让任意服务端函数的结果可缓存,不限于 HTTP fetchcacheTag()为缓存内容打标签,支持动态标签(基于参数)cacheLife()控制缓存过期策略(stale + revalidate 双模式)revalidateTag()统一清除 fetch 缓存和 Content Cache 中的对应标签- 隐式 fetch 缓存(Data Cache)适合 HTTP 请求,显式 use cache(Content Cache)适合任意函数
- 缓存粒度从粗到细:页面级 → 数据级 → 函数级,use cache 实现函数级缓存
- 最佳实践:对数据库查询、复杂计算、数据处理函数使用 use cache,对 HTTP API 使用 fetch 缓存
📝 作业
-
基础题(⭐):创建一个
app/cache-demo/page.tsx,用一个'use cache'函数包装一个模拟耗时操作(await new Promise(resolve => setTimeout(resolve, 2000))),在页面中调用 3 次,验证第二次开始零延迟(缓存命中)。 -
进阶题(⭐⭐):构建一个包含用户列表 + 用户文章列表的页面。用户列表使用
cacheLife({ revalidate: 300 }),文章列表使用cacheLife({ revalidate: 60 })。在页面底部添加两个 Server Action 按钮,分别刷新用户标签和文章标签。验证标签独立失效。 -
挑战题(⭐⭐⭐):实现一个"数据分析看板",从三个不同数据源获取数据(HTTP API + 数据库模拟 + 计算函数),使用 use cache 统一缓存。每个数据源有不同的 cacheLife 策略。添加一个"Force Refresh All"按钮调用
revalidateTag同时刷新所有标签。构建缓存命中率统计工具。