文章目录
- 前言
- Guidelines
- [High-Impact Server](#High-Impact Server)
-
- [1. Use React.cache() for per-request deduplication](#1. Use React.cache() for per-request deduplication)
-
- 核心问题
- [反例:同一请求,多次 fetch](#反例:同一请求,多次 fetch)
- 推荐:`React.cache`
- 实际发生了什么?
- [适合 cache 的内容](#适合 cache 的内容)
- 一句话总结
- [2. Use LRU cache for cross-request caching](#2. Use LRU cache for cross-request caching)
-
- 核心问题
- 典型场景
- 反例:每个请求都重新算
- [推荐:LRU Cache](#推荐:LRU Cache)
- [和 React.cache 的关系(非常重要)](#和 React.cache 的关系(非常重要))
- 单请求和跨请求概念
- [3. Minimize serialization at RSC boundaries](#3. Minimize serialization at RSC boundaries)
- [4. Parallelize data fetching with component composition](#4. Parallelize data fetching with component composition)
-
- [反例:集中式 data fetching](#反例:集中式 data fetching)
- [推荐:组件各自 fetch](#推荐:组件各自 fetch)
- 实际发生了什么?
- 把两部分合在一起看
-
- 第一部分:控制时间线
- [第二部分:控制重复 & 边界](#第二部分:控制重复 & 边界)
- [一句话总结(Vercel Server 思维)](#一句话总结(Vercel Server 思维))
前言
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Guidelines

在这个系列,我会逐条拆解,每一条都给出:
- 核心问题是什么
- 为什么会慢(本质原因)
- 典型业务场景
- 反例代码
- 推荐写法
- 在 React / Next.js 中的实际收益
High-Impact Server
这是系列的第二部分。
这一部分**已经进入 Vercel / React Server Components 的"真·内功区"**了。它解决的不是「代码好不好看」,而是:
同一请求内别算两遍、不同请求别老算、别在 RSC 边界浪费、让组件结构天然并行
1. Use React.cache() for per-request deduplication
「同一个请求里,只算一次」
核心问题
在 同一次页面请求 中:
- 多个组件
- 多个层级
- 多次调用 同一个数据函数
会被重复执行
反例:同一请求,多次 fetch
ts
async function getUser() {
return fetch('/api/user').then(r => r.json())
}
ts
// Header.tsx
const Header = async () => {
const user = await getUser()
return <div>{user.name}</div>
}
// Sidebar.tsx
const Sidebar = async () => {
const user = await getUser()
return <Avatar user={user} />
}
结果:
- 同一个请求
- 发了 2 次
/api/user - 纯浪费
推荐:React.cache
ts
import { cache } from 'react'
export const getUser = cache(async () => {
return fetch('/api/user').then(r => r.json())
})
ts
// Header / Sidebar 仍然各自调用
实际发生了什么?
- 第一次调用:真正执行
- 后续调用:直接复用结果
- 作用域:单个请求(request-scoped)
不会跨用户、不会脏数据
适合 cache 的内容
- 用户信息
- 权限 / role
- feature flags
- 请求上下文数据
不适合:
- 强依赖实时性的(秒级行情)
- 非纯函数(依赖时间、随机数)
一句话总结
React.cache()= request-level memoization
2. Use LRU cache for cross-request caching
「不同请求之间复用结果」
核心问题
React.cache() 只在单次请求有效
但有些数据:
- 变化不频繁
- 计算成本高
- 用户之间通用
应该跨请求缓存
典型场景
- 产品配置
- 权限模型
- 城市 / 国家列表
- Markdown → HTML
- OpenAPI schema
反例:每个请求都重新算
ts
async function getConfig() {
return expensiveCompute()
}
推荐:LRU Cache
ts
import LRU from 'lru-cache'
const cache = new LRU<string, any>({
max: 500,
ttl: 1000 * 60 * 5, // 5 分钟
})
export async function getConfig() {
const cached = cache.get('config')
if (cached) return cached
const data = await expensiveCompute()
cache.set('config', data)
return data
}
和 React.cache 的关系(非常重要)
| 能力 | React.cache | LRU |
|---|---|---|
| 作用范围 | 单请求 | 跨请求 |
| 生命周期 | request | 进程 |
| 适合数据 | user / ctx | 公共数据 |
| 安全性 | 极高 | 需注意 |
真实项目中:经常一起用
ts
export const getUser = cache(async (id) => {
return userLRU.get(id) ?? fetchAndSet(id)
})
如果你还是无法直接理解单请求和跨请求的区别,请继续阅读下面这部分内容。
单请求和跨请求概念
React.cache = 只在「这一次页面请求」里生效
LRU cache = 在「多次页面请求」之间生效
一、什么叫「单请求(per-request)」?
在 Next.js / RSC 中:
一次浏览器请求一个页面 = 一次 Server Rendering 请求 = 一个 React Server 渲染上下文
例如:
用户 A 打开 /dashboard ← 请求 #1
用户 A 刷新 /dashboard ← 请求 #2
用户 B 打开 /dashboard ← 请求 #3
这 3 个请求是完全独立的。
二、React.cache:为什么叫「单请求」?
看代码
ts
import { cache } from 'react'
export const getUser = cache(async (id) => {
console.log('fetch user', id)
return db.user.find(id)
})
在同一次请求中
ts
const Header = async () => {
const user = await getUser(1)
}
const Sidebar = async () => {
const user = await getUser(1)
}
实际输出
fetch user 1 ✅ 只打印一次
原因:
- React 在「当前请求上下文」里
- 维护了一张 cache map
- key = 函数 + 参数
- 请求结束 → cache 自动销毁
换个请求会怎样?
用户刷新页面
fetch user 1 ❗️又打印了一次
也就是说 React.cache 不会跨请求。
三、为什么 React.cache 不能跨请求?(非常重要)
因为 安全 & 正确性。如果能跨请求,会发生什么?
用户 A 请求 → getUser(1) → 缓存
用户 B 请求 → 复用了 A 的 user
严重数据泄露。
所以 React.cache 的设计目标是:
避免同一次渲染中重复计算
❌ 不是做"数据缓存系统"
四、LRU cache:什么叫「跨请求」?
LRU 是什么?
进程级内存缓存
ts
const cache = new LRU({
max: 100,
ttl: 1000 * 60,
})
它存在于:
- Node.js 进程内存
- 不会因请求结束而清空
多次请求会命中同一份缓存:
ts
export async function getConfig() {
if (cache.has('config')) {
return cache.get('config')
}
const data = await loadConfig()
cache.set('config', data)
return data
}
请求 #1 → loadConfig()
请求 #2 → 命中 cache
请求 #3 → 命中 cache
这就是"跨请求"。
六、什么时候用哪个?(直接可用表)
| 场景 | React.cache | LRU |
|---|---|---|
| 同一页面多组件用 user | ✅ | ❌ |
| 防止同请求重复 fetch | ✅ | ❌ |
| 公共配置 | ❌ | ✅ |
| 城市列表 | ❌ | ✅ |
| 用户私有数据 | ✅ | ⚠️ |
| 跨用户共享数据 | ❌ | ✅ |
七、为什么这和 RSC 特别相关?
因为在 RSC 中:
- 组件 = async 函数
- 同一个函数会被多次调用
- 调用顺序由 React 调度
- 你很难保证只调用一次
一句话帮你"刻进脑子里":
React.cache:React 帮你去重
LRU:你帮服务器省钱
3. Minimize serialization at RSC boundaries
「别在 Server → Client 边界传大对象」
核心问题
Server Component → Client Component:
- 数据会被 JSON 序列化
- 再反序列化
- 再进入 hydration
大对象 = 性能杀手
反例:传整个对象
ts
// Server Component
const user = await getUser()
return <ClientProfile user={user} />
ts
// user = { id, name, email, roles, permissions, history, ... }
推荐:只传必要字段
ts
return (
<ClientProfile
userId={user.id}
name={user.name}
/>
)
更进一步:Server 端消化逻辑
把逻辑丢给 Client
ts
<ClientChart rawData={bigData} />
Server 先算好
ts
const chartData = processChartData(bigData)
<ClientChart data={chartData} />
一个非常重要的原则
Client Component = 交互
Server Component = 计算 + IO
4. Parallelize data fetching with component composition
「组件结构 = 并行结构」
这是 RSC 最"反直觉但最强"的能力。
反例:集中式 data fetching
ts
const Page = async () => {
const user = await getUser()
const posts = await getPosts()
const stats = await getStats()
return (
<>
<Profile user={user} />
<PostList posts={posts} />
<Stats stats={stats} />
</>
)
}
隐性问题:
- Page 被迫成为"数据瓶颈"
- Suspense 不好拆
- 并行受限
推荐:组件各自 fetch
ts
const Profile = async () => {
const user = await getUser()
return <div>{user.name}</div>
}
const PostList = async () => {
const posts = await getPosts()
return <ul>{/* ... */}</ul>
}
const Stats = async () => {
const stats = await getStats()
return <Chart data={stats} />
}
ts
const Page = () => {
return (
<>
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
<Suspense fallback={<PostSkeleton />}>
<PostList />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
</>
)
}
实际发生了什么?
- React 同时启动所有组件的 async
- 自动并行
- 自动 streaming
- 自动局部 fallback
你只负责组件拆分,React 负责调度
把两部分合在一起看
第一部分:控制时间线
WHAT - Vercel react-best-practices 系列(一)
- Start promises early
- Promise.all
- Suspense
第二部分:控制重复 & 边界
- cache(请求内)
- LRU(请求间)
- 减少 RSC payload
- 组件并行
两者叠加,才是 Vercel 推荐的"正确打开方式"
一句话总结(Vercel Server 思维)
数据靠近组件
缓存靠近计算
并行来自结构
Client 只做交互