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

少订阅

少算

慢更新

相关推荐
kyriewen7 小时前
写组件文档写到吐?我用AI自动生成Storybook,同事以后直接抄
前端·javascript·面试
五点六六六8 小时前
你敢信这是非Native页面写出来的渐变效果吗🌝(底层原理解析
前端·javascript·面试
吃西瓜的年年9 小时前
TypeScript
javascript·ubuntu·typescript
熊猫_豆豆11 小时前
一个模拟四轴飞行器在随机气流扰动下悬停飞行的交互式3D仿真网页,包含飞行器建模与PID控制算法
javascript·3d·html·四轴无人机模拟飞行
来恩100312 小时前
jQuery选择器
前端·javascript·jquery
前端繁华如梦13 小时前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo13 小时前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE14 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家14 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班14 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html