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

少订阅

少算

慢更新

相关推荐
duangww1 小时前
JavaScript调用ABAP后端发布的restful服务
javascript·sap fiori
答案—answer2 小时前
开源项目:Three.js3D模型可视化编辑系统
javascript·3d·开源·开源项目·three.js·three.js编辑器
白兰地空瓶2 小时前
Zustand:若 React 组件是公民,谁来当“中央银行”?—— 打造轻量级企业级状态管理
react.js·typescript
Thomas游戏开发2 小时前
分享一个好玩的:一次提示词让AI同时开发双引擎框架
前端·javascript·后端
NEXT062 小时前
别再折磨自己了!放弃 Redux 后,我用 Zustand + TS 爽到起飞
前端·react.js
m0_748252382 小时前
Angular 2 数据显示方法
前端·javascript·angular.js
2501_944711432 小时前
现代 React 路由实践指南
前端·react.js·前端框架
蓁蓁啊2 小时前
GCC 头文件搜索路径:-I vs -idirafter 深度解析
java·前端·javascript·嵌入式硬件·物联网
依赖_赖2 小时前
前端实现token无感刷新
前端·javascript·vue.js