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 中的实际收益

Medium-Impact

这是系列的第三部分。

这一部分开始从"Server 极致性能"回到"Client 交互体验",重点不再是 RTT,而是:

减少不必要的 re-render、避免同步阻塞、让用户感觉"很跟手"

1. Use SWR for automatic request deduplication

「同一时间只发一次请求」

核心问题

在 Client 侧:

  • 多个组件
  • 同一个接口
  • 同时 mount

浏览器会发多次相同请求

反例:手写 useEffect

ts 复制代码
function useUser() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/user')
      .then(r => r.json())
      .then(setData)
  }, [])

  return data
}
ts 复制代码
<Header />
<Sidebar />

/api/user 被请求两次

推荐:SWR 自动去重

ts 复制代码
import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

function useUser() {
  return useSWR('/api/user', fetcher)
}

SWR 做了什么?

  • 同 key 只请求一次
  • 多组件共享结果
  • 自动缓存 & revalidate

一句话总结

SWR = Client 版 React.cache

2. Defer state reads to usage point

「不要为了"可能用"而提前读 state」

核心问题

React 中:

  • 读取 state = 建立订阅
  • state 更新 → 组件 re-render

提前读 = 不必要的 re-render

反例:提前解构

ts 复制代码
const { user, theme, locale } = useAppStore()

即使只用 theme:

  • user 更新
  • locale 更新

组件都会重渲染。

推荐:用到再读

ts 复制代码
const theme = useAppStore(state => state.theme)

Zustand / Redux / Jotai 都适用

ts 复制代码
useSelector(state => state.user.name)

一句话总结

订阅越小,重渲染越少

3. Use lazy state initialization for expensive values

「初始值贵,就别每次算」

核心问题

ts 复制代码
useState(expensiveCompute())
  • 每次 render 都会执行 expensiveCompute
  • 即使只用第一次

反例

ts 复制代码
const [value] = useState(buildBigMap(data))

推荐:lazy init

ts 复制代码
const [value] = useState(() => buildBigMap(data))

函数只在初次 render 执行一次。

useState 的两种"初始化模式":

  1. 普通初始化(每次 render 都会算),也就是 useState(buildBigMap(data)),React 在 render 前,先执行 buildBigMap,把执行结果作为参数传给 useState,每次 render 都会执行一次。

  2. Lazy 初始化,也就是 useState(() => buildBigMap(data)),传递给 useState 的是一个 Initializer function,内部处理如下

typescript 复制代码
function useState(initialState) {
  if (isFirstRender) {
    if (typeof initialState === 'function') {
      state = initialState()
    } else {
      state = initialState
    }
  }
  return [state, setState]
}

因此,只有在 first render(mount)时:才会执行 initialState(),后续 render:直接读取已经保存的 state,不会再碰这个函数。

典型场景

  • JSON.parse
  • 构建索引 Map
  • 复杂正则
  • 大数组预处理

一句话总结

初始 state = 函数

4. Use derived state subscriptions

「不要存"算得出来的 state"」

核心问题

ts 复制代码
const [filtered, setFiltered] = useState([])

但 filtered 明明来自 list + keyword

双源真相,必出 bug

反例

ts 复制代码
useEffect(() => {
  setFiltered(list.filter(i => i.name.includes(keyword)))
}, [list, keyword])

推荐:派生 state

ts 复制代码
const filtered = useMemo(() => {
  return list.filter(i => i.name.includes(keyword))
}, [list, keyword])

在全局状态库中更重要

ts 复制代码
useStore(state => state.items.filter(i => i.active))

一句话总结

能算出来,就别存

5. Apply startTransition for non-urgent updates

「区分"着急"和"不着急"的更新」

React 18 引入了 更新优先级(lanes)

  • Urgent lane:输入、点击、focus
  • Transition lane:可延后更新

核心问题

React 默认:

  • 所有 state 更新都是"紧急的"
  • 大量更新 → 卡顿

典型场景

  • 搜索过滤
  • 表格排序
  • 列表分页
  • tab 切换后加载数据

反例:同步更新

ts 复制代码
onChange={(e) => {
  setKeyword(e.target.value)
  setFilteredData(filter(data, e.target.value))
}}

输入会卡。因为 React 内部的理解是:"这两个 setState 同等重要,必须立刻算完",如果 filter(data) 很重:

  1. 输入法卡
  2. 光标延迟
  3. 掉帧

二、startTransition 到底做了什么?

推荐:startTransition

ts 复制代码
import { startTransition } from 'react'

onChange={(e) => {
  setKeyword(e.target.value)

  startTransition(() => {
    setFilteredData(filter(data, e.target.value))
  })
}}

效果:

  • 输入优先
  • 列表稍后更新
  • UI 更丝滑

startTransition = 告诉 React:「这次更新不着急,别挡住用户操作」。没有 startTransition

typescript 复制代码
用户输入
↓
React:必须算完 filter(500ms)
↓
UI 更新

使用 startTransition

typescript 复制代码
用户输入
↓
React:先更新 input(5ms)
↓
空闲时再算 filter
↓
更新列表

注意,它不是 setTimeout,比如 setTimeout(() => setList(...), 0),也不是 debounce。

对比 setTimeout startTransition
是否理解 UI ❌ ✅
可被中断 ❌ ✅
与调度器协作 ❌ ✅
与 Concurrent Rendering ❌ ✅

更具体的对比可以阅读:WHAT - React startTransition vs setTimeout vs debounce

什么时候该用 startTransition

场景 是否适合
搜索过滤 ✅
表格排序 ✅
分页切换 ✅
Tab 内容切换 ✅
输入框 value ❌
hover 状态 ❌
modal 开关 ❌

总结

它们解决的是 三类问题

1️⃣ 请求重复 → SWR

2️⃣ 订阅过多 → defer reads / derived state

3️⃣ 计算 & 更新阻塞 → lazy init / transition

一句话 Client 性能心法

少订阅

少算

慢更新

相关推荐
默_笙29 分钟前
🧀 让大模型的答案"流"出来:NestJS AI 模块 + SSE + RxJS 实战
javascript·人工智能
mbshqqb31 分钟前
TypeScript基础教程之同步生成器
前端·javascript·typescript
尤水就下1 小时前
🔥预热:在线文档的 block 架构是如何实现的(未完待续...)
前端·javascript·架构
song5011 小时前
React Native for OpenHarmony 实战:三方库 react-native-parallax-scroll-view 的鸿蒙化适配指南
分布式·react native·react.js·transformer·harmonyos
全栈项目管理程序猿2 小时前
ArcGIS JS 基础教程(29):CSVLayer 表格点位图层
前端·javascript
骑着蜗牛撵大象3272 小时前
豆包 Seed 驱动的记账智能体:从拍票抽取到月报生成的全链路落地记录
java·前端·javascript·结构化输出·豆包seed·多模态识别·记账系统
溪语流沙2 小时前
【Web全栈进阶】前端世界观:现代工具链与框架为什么存在
前端·javascript·vue.js
kyriewen3 小时前
我逐个测了 JSON.stringify 的 7 个坑:第 1 个就在丢数据
前端·javascript·程序员
song5013 小时前
React Native for OpenHarmony 实战:三方库 react-native-ratings 的鸿蒙化适配指南
javascript·react native·react.js·分类·harmonyos
朱包林4 小时前
iTerm2终端配置指南
linux·javascript·python·云原生·容器·kubernetes·云计算