WHAT - Vercel react-best-practices 系列(二)

文章目录

前言

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

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 只做交互

相关推荐
qq_406176141 小时前
深入理解 JavaScript 闭包:从原理到实战避坑
开发语言·前端·javascript
float_六七2 小时前
JavaScript变量声明:var的奥秘
开发语言·前端·javascript
zhengxianyi5152 小时前
ruoyi-vue-pro本地环境搭建(超级详细,带异常处理)
前端·vue.js·前后端分离·ruoyi-vue-pro
桃子叔叔2 小时前
react-wavesurfer录音组件1:从需求到组件一次说清楚
前端·react.js·前端框架
陈随易2 小时前
聊一聊2025年用AI的思考与总结
前端·后端·程序员
@PHARAOH2 小时前
WHAT - React startTransition vs setTimeout vs debounce
前端·react.js·前端框架
研☆香2 小时前
JavaScript 特点介绍
开发语言·javascript·ecmascript
绝美焦栖2 小时前
低版本pdfjs升级
前端·javascript·vue.js
阿里巴巴终端技术2 小时前
二十年,重新出发!第 20 届 D2 技术大会「AI 新」议题全球征集正式开启
前端·react.js·html