一句话总结:它是专为 React 服务器组件(RSC)设计的“记忆化”利器,能让你在同一个请求中,多次调用同一个函数,却只执行一次真实逻辑。
想象一下这个场景:你点了三份同样的外卖(同一篇博客文章的数据),商家会让三个厨师各做一遍,还是让一个厨师做一份然后分装三份?当然是后者!React.cache 就是那个帮你“只做一份”的调度员。
一、🤔 为什么需要 React.cache?从“外卖比喻”说起
在 Next.js 的 App Router 中,一个页面通常由多个组件组成:布局、页面本身、generateMetadata……每个组件都可能需要同样的数据。
没有缓存时:
// 这三个地方都要用到文章数据,每次都重新查数据库!😱
// app/article/layout.tsx
const { title } = await getArticle() // 查第一次
// app/article/page.tsx
const { title } = await getArticle() // 查第二次
// app/article/layout.tsx 的 generateMetadata
const { title } = await getArticle() // 查第三次
结果是:三次数据库查询,三次网络请求,三次等待时间。 这在复杂页面里就是性能灾难。
有了 React.cache 之后:
// utils.ts
import { cache } from 'react'
export const getArticle = cache(async (id) => {
return await db.article.findUnique({ where: { id } })
})
无论你在多少个组件里调用 getArticle(id),只要 id 相同,真正的数据库查询只执行一次,后续调用直接返回缓存结果。
二、📦 核心 API:超级简单
React.cache 的 API 极其简洁,长这样:
import { cache } from 'react'
const cachedFn = cache(originalFn)
- 入参:一个函数(可以是同步或异步的)
- 返回值:一个具有相同签名的“记忆化”版本
- 缓存依据:函数调用的所有参数(严格相等比较
Object.is)
看个实际例子:
import { cache } from 'react'
const fetchWeather = async (city) => {
console.log(`🌤️ 真实请求: ${city}`)
const res = await fetch(`https://api.weather.com/${city}`)
return res.json()
}
const getCachedWeather = cache(fetchWeather)
// 第一次调用:执行真实请求
const nyc1 = await getCachedWeather('New York') // 日志输出
// 第二次调用(相同参数):直接返回缓存,不执行函数
const nyc2 = await getCachedWeather('New York') // 无日志
// 不同参数:重新执行
const london = await getCachedWeather('London') // 日志输出
注意:缓存不仅存成功结果,也缓存错误。如果第一次调用抛异常,后续相同参数的调用也会抛出同样的异常。
三、🎯 实战场景一:共享数据快照
这是 RSC 中最经典的使用场景:多个组件需要同一份数据。
在掘金的一篇实战文章中,作者展示了这样一个结构:
// app/article/utils.ts
import { cache } from 'react'
import { db } from '@/lib/db'
export const getArticle = cache(async (id: string) => {
// 模拟耗时的数据库查询
await new Promise(r => setTimeout(r, 2000))
return await db.article.findUnique({ where: { id } })
})
然后在布局和页面中同时使用:
// app/article/[id]/layout.tsx
import { getArticle } from './utils'
export default async function Layout({ params: { id } }) {
const { title } = await getArticle(id) // ← 调用1
return (
<div className="banner">
您正在阅读:{title}
{children}
</div>
)
}
// app/article/[id]/page.tsx
import { getArticle } from './utils'
export default async function Page({ params: { id } }) {
const { title } = await getArticle(id) // ← 调用2,走缓存
return <h1>{title}</h1>
}
效果:两个组件虽然都调用了 getArticle,但数据库只查询了一次。而且因为用的是同一个记忆化函数,它们拿到的数据快照完全一致——不会出现布局显示“标题A”,页面显示“标题B”的乌龙。
四、⚡ 实战场景二:预加载数据(Preload Pattern)
这是官方文档特别推荐的一个高阶技巧:在组件真正需要数据之前,提前发起请求,利用缓存把数据“预热”。
import { cache } from 'react'
import { db } from '@/lib/db'
const getUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } })
})
// 预加载函数:只调用,不 await
function preloadUser(id: string) {
void getUser(id) // 启动数据获取,但不等待
}
// 实际使用的组件
async function UserProfile({ id }: { id: string }) {
const user = await getUser(id) // 如果预加载已完成,这里几乎瞬间返回
return <div>{user.name}</div>
}
// 页面组件
export default async function Page({ params }: { params: { id: string } }) {
preloadUser(params.id) // ← 立即开始获取用户数据
// ... 其他计算工作 ...
return <UserProfile id={params.id} /> // ← 用到时可能已经在缓存里了
}
这个模式的好处是并行化:在等待预加载数据的同时,React 可以继续执行其他计算或渲染其他组件,而不是串行等待。
五、⚠️ 三个你必须知道的陷阱
陷阱1:React.cache 只适用于服务器组件
这是官方文档明确强调的:cache 仅供与 React 服务器组件一起使用。在客户端组件('use client')里用,不会报错,但缓存不生效。
为什么?因为缓存的访问是通过 React 内部的**请求上下文(request storage)**实现的,而这个上下文只在服务器端渲染时才存在。
社区里已经有人在 Next.js Discord 里问过这个问题:想把 React.cache 用在客户端组件里做请求去重,结果是——不管用。建议用 React Query、SWR 等专门的客户端缓存方案。
陷阱2:不同记忆化函数,不共享缓存
这是最容易踩的坑!看这段错误代码:
// ❌ 错误示例
function ComponentA() {
const getData = cache(fetchData) // 每次渲染创建新的缓存函数
const data = getData(id)
}
function ComponentB() {
const getData = cache(fetchData) // 又一个独立缓存函数
const data = getData(id)
}
两个组件各创自己的记忆化函数,缓存互相隔离,重复劳动依然发生。
正确做法:把 cache 调用放在模块顶层,导出一个共享的缓存函数:
// ✅ 正确:导出同一个缓存函数
// utils/data.ts
import { cache } from 'react'
export const getData = cache(fetchData)
// 组件A和B都 import 同一个 getData
陷阱3:在组件外部调用,不触发缓存
import { cache } from 'react'
const getUser = cache(async (id) => {
return await db.user.findUnique({ where: { id } })
})
// ❌ 在组件外部调用,不会写入缓存
await getUser('123')
export default async function Page() {
// ✅ 在组件内部调用,会使用缓存
const user = await getUser('123')
}
React 只在组件渲染期间提供缓存上下文,外部调用虽然能执行函数,但缓存不会被读取或写入。
六、📊 cache vs useMemo vs memo:一张图看懂
很多初学者会混淆这几种“记忆化”机制,官方文档给了很清晰的区分:
| API | 适用场景 | 缓存范围 | 缓存依据 | 生命周期 |
|---|---|---|---|---|
React.cache | 服务器组件 | 跨组件共享 | 函数参数 | 单次请求 |
useMemo | 客户端组件 | 单个组件实例 | 依赖数组 | 组件生命周期 |
React.memo | 客户端组件 | 组件渲染结果 | Props 浅比较 | 组件生命周期 |
简单理解:
React.cache:“一个请求内,所有人共用一份”(餐厅版:一个厨师做一份菜,分给三桌客人)useMemo:“一个组件内,重复渲染不重算”(餐厅版:同一桌客人反复点同一道菜,后厨不重做)React.memo:“Props 没变就不重渲染”(餐厅版:客人没换菜,服务员就不重新下单)
七、🔗 与 Next.js 的深度集成
在 Next.js 中,React.cache 是官方推荐的数据库查询缓存方案。而且 Next.js 还在持续优化它们的配合:
- 自动去重:最新版本的 Next.js 修复了在
"use cache"函数中使用React.cache时的去重问题 - 静态渲染 + 缓存:当页面使用
export const revalidate = 3600时,React.cache会在每次重新渲染时清空缓存,配合revalidate实现定时更新
GitHub Trending 上的热门 UI 项目也大量使用 React.cache 来缓存组件文档和代码高亮的结果,提升首屏加载速度达 75%。
八、🎯 什么时候用 React.cache?
| ✅ 推荐场景 | ❌ 不推荐场景 |
|---|---|
| 在 RSC 中多次调用同一个数据获取函数 | 在客户端组件中 |
| 布局 + 页面 + 元数据都需要同一份数据 | 数据只在单个组件中使用一次 |
| 预加载数据,提升性能 | 需要跨请求持久化缓存(用 Redis/CDN) |
| 数据库查询、API 调用、复杂计算 | 数据在客户端频繁变化 |
九、💡 总结:一张“外卖调度单”
回到开头的比喻。React.cache 就像餐厅里的智能调度系统:
- 它记住了“谁点了什么菜”
- 发现重复订单时,直接复用已有的备菜
- 所有菜品在同一批次送达,保证口味一致
核心记忆点:
- 适用场景:仅限 React 服务器组件(RSC)
- 缓存依据:函数的所有参数(严格相等)
- 共享规则:必须使用同一个记忆化函数实例
- 生命周期:每次请求独立,请求结束缓存清空