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 性能心法

少订阅

少算

慢更新

相关推荐
2501_9219308312 小时前
基础入门 React Native 鸿蒙跨平台开发:react-native-button三方库适配
react native·react.js·harmonyos
yuezhilangniao20 小时前
AI智能体全栈开发工程化规范 备忘 ~ fastAPI+Next.js
javascript·人工智能·fastapi
铅笔侠_小龙虾21 小时前
Flutter Demo
开发语言·javascript·flutter
2501_9445255421 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 账户详情页面
android·java·开发语言·前端·javascript·flutter
wangdaoyin201021 小时前
若依vue2前后端分离集成flowable
开发语言·前端·javascript
天天进步20151 天前
AI Agent 与流式处理:Motia 在生成式 AI 时代的后端范式
javascript
心柠1 天前
vue3相关知识总结
前端·javascript·vue.js
常年游走在bug的边缘1 天前
掌握JavaScript作用域:从函数作用域到块级作用域的演进与实践
开发语言·前端·javascript
极致♀雨1 天前
vue2+elementUI table表格勾选行冻结/置顶
前端·javascript·vue.js·elementui
林shir1 天前
3-15-前端Web实战(Vue工程化+ElementPlus)
前端·javascript·vue.js