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

相关推荐
代码搬运媛6 小时前
Jest 测试框架详解与实现指南
前端
counterxing7 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq7 小时前
windows下nginx的安装
linux·服务器·前端
之歆8 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜8 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108088 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong8 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
kyriewen10 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm10 小时前
元框架的工作原理详解
前端·前端框架
canonical_entropy10 小时前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程