文章目录
- 前言
- Guidelines
- High-Impact Server
- 1. Use React.cache() for per-request deduplication
- 核心问题
- 反例:同一请求,多次 fetch
- 推荐:`React.cache`
- 实际发生了什么?
- 适合 cache 的内容
- 一句话总结
- 2. Use LRU cache for cross-request caching
- 核心问题
- 典型场景
- 反例:每个请求都重新算
- 推荐:LRU Cache
- 和 React.cache 的关系(非常重要)
- 单请求和跨请求概念
- 3. Minimize serialization at RSC boundaries
- 核心问题
- 反例:传整个对象
- 推荐:只传必要字段
- 更进一步:Server 端消化逻辑
- 一个非常重要的原则
- 4. Parallelize data fetching with component composition
- 反例:集中式 data fetching
- 推荐:组件各自 fetch
- 实际发生了什么?
- 把两部分合在一起看
- 第一部分:控制时间线
- 第二部分:控制重复 & 边界
- 一句话总结(Vercel Server 思维)
前言
react-best-practices
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
asyncfunctiongetUser(){returnfetch('/api/user').then(r=>r.json())}// Header.tsxconstHeader=async()=>{constuser=awaitgetUser()return<div>{user.name}</div>}// Sidebar.tsxconstSidebar=async()=>{constuser=awaitgetUser()return<Avatar user={user}/>}结果:
- 同一个请求
- 发了2 次
/api/user - 纯浪费
推荐:React.cache
import{cache}from'react'exportconstgetUser=cache(async()=>{returnfetch('/api/user').then(r=>r.json())})// 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
反例:每个请求都重新算
asyncfunctiongetConfig(){returnexpensiveCompute()}推荐:LRU Cache
importLRUfrom'lru-cache'constcache=newLRU<string,any>({max:500,ttl:1000*60*5,// 5 分钟})exportasyncfunctiongetConfig(){constcached=cache.get('config')if(cached)returncachedconstdata=awaitexpensiveCompute()cache.set('config',data)returndata}和 React.cache 的关系(非常重要)
| 能力 | React.cache | LRU |
|---|---|---|
| 作用范围 | 单请求 | 跨请求 |
| 生命周期 | request | 进程 |
| 适合数据 | user / ctx | 公共数据 |
| 安全性 | 极高 | 需注意 |
真实项目中:经常一起用
exportconstgetUser=cache(async(id)=>{returnuserLRU.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:为什么叫「单请求」?
看代码
import{cache}from'react'exportconstgetUser=cache(async(id)=>{console.log('fetch user',id)returndb.user.find(id)})在同一次请求中
constHeader=async()=>{constuser=awaitgetUser(1)}constSidebar=async()=>{constuser=awaitgetUser(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 是什么?
进程级内存缓存
constcache=newLRU({max:100,ttl:1000*60,})它存在于:
- Node.js 进程内存
- 不会因请求结束而清空
多次请求会命中同一份缓存:
exportasyncfunctiongetConfig(){if(cache.has('config')){returncache.get('config')}constdata=awaitloadConfig()cache.set('config',data)returndata}请求 #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
大对象 = 性能杀手
反例:传整个对象
// Server Componentconstuser=awaitgetUser()return<ClientProfile user={user}/>// user = { id, name, email, roles, permissions, history, ... }推荐:只传必要字段
return(<ClientProfile userId={user.id}name={user.name}/>)更进一步:Server 端消化逻辑
把逻辑丢给 Client
<ClientChart rawData={bigData}/>Server 先算好
constchartData=processChartData(bigData)<ClientChart data={chartData}/>一个非常重要的原则
Client Component = 交互
Server Component = 计算 + IO
4. Parallelize data fetching with component composition
「组件结构 = 并行结构」
这是RSC 最“反直觉但最强”的能力。
反例:集中式 data fetching
constPage=async()=>{constuser=awaitgetUser()constposts=awaitgetPosts()conststats=awaitgetStats()return(<><Profile user={user}/><PostList posts={posts}/><Stats stats={stats}/></>)}隐性问题:
- Page 被迫成为“数据瓶颈”
- Suspense 不好拆
- 并行受限
推荐:组件各自 fetch
constProfile=async()=>{constuser=awaitgetUser()return<div>{user.name}</div>}constPostList=async()=>{constposts=awaitgetPosts()return<ul>{/* ... */}</ul>}constStats=async()=>{conststats=awaitgetStats()return<Chart data={stats}/>}constPage=()=>{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 只做交互