Next.js: Cache Components 与 use cache

最后更新:2026-08-26

use cache 是 Next.js 16 最具变革性的 API——它把缓存从"数据获取的副作用"升级为"组件级的一等公民"。

1. 你将学到


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() 包裹函数——缓存内部的任何函数调用,包括数据库查询、计算、文件读取。

TSX
// 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 缓存)。

100%
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) 基本语法

TSX
// 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 用法(难度⭐)

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

输出:

TEXT 📖 仅展示
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) 打标签分类策略

100%
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) 动态标签

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

▶ 示例:分层缓存策略(难度⭐⭐)

TSX
// 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 失效(难度⭐⭐)

TSX
// 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 整块逻辑打包缓存

▶ 示例:两种缓存的组合使用(难度⭐⭐⭐)

TSX
// 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. 完整示例:用户管理系统的缓存架构

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

▶ 示例:验证缓存命中(难度⭐⭐)

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

输出:

TEXT 📖 仅展示
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)  ← 缓存命中

❓ 常见问题

Q use cacheuseMemo 有什么区别?
A useMemo 是客户端 Hook,只在浏览器中缓存计算、作用域单次渲染。use cache 是服务端指令,跨请求持久化缓存、可带标签和过期时间、支持 Server Action 按需失效。两者完全不同的使用场景。
Q 可以同时在 fetch 和 use cache 上使用相同的 tag 吗?
A 可以。revalidateTag('products') 会同时清除 fetch Data Cache 中带有 next: { tags: ['products'] } 的条目和 Content Cache 中 cacheTag('products') 的条目。这是实现统一缓存失效的关键机制。
Q use cache 可以用于 Client Component 吗?
A 不可以。'use cache' 指令只在 Server Component 或服务端函数中有效。客户端组件中应使用 useMemo 或 React Query 等客户端缓存方案。
Q unstable_cacheLifeunstable_cacheTagunstable_ 前缀意味着什么?
A 这意味着 API 仍在迭代中,未来版本可能修改。Next.js 16.2 中它们是可用的,但建议关注官方更新。unstable_ 前缀通常会在 1-2 个主版本后移除(预计 Next.js 17 或 18 稳定)。
Q Content Cache 的数据存在哪里?
A Content Cache 默认存储在内存中(生产环境跨请求持久化)。在 Vercel 部署时,它会利用边缘存储。自托管时存储在文件系统或内存中。缓存大小受服务器内存限制,大量缓存会占用内存资源。

📖 小节


📝 作业

  1. 基础题(⭐):创建一个 app/cache-demo/page.tsx,用一个 'use cache' 函数包装一个模拟耗时操作(await new Promise(resolve => setTimeout(resolve, 2000))),在页面中调用 3 次,验证第二次开始零延迟(缓存命中)。

  2. 进阶题(⭐⭐):构建一个包含用户列表 + 用户文章列表的页面。用户列表使用 cacheLife({ revalidate: 300 }),文章列表使用 cacheLife({ revalidate: 60 })。在页面底部添加两个 Server Action 按钮,分别刷新用户标签和文章标签。验证标签独立失效。

  3. 挑战题(⭐⭐⭐):实现一个"数据分析看板",从三个不同数据源获取数据(HTTP API + 数据库模拟 + 计算函数),使用 use cache 统一缓存。每个数据源有不同的 cacheLife 策略。添加一个"Force Refresh All"按钮调用 revalidateTag 同时刷新所有标签。构建缓存命中率统计工具。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏