文章目录
- 前言
- Guidelines
- Medium-Impact
- 1. Use SWR for automatic request deduplication
- 核心问题
- 反例:手写 useEffect
- 推荐:SWR 自动去重
- 一句话总结
- 2. Defer state reads to usage point
- 核心问题
- 反例:提前解构
- 推荐:用到再读
- Zustand / Redux / Jotai 都适用
- 一句话总结
- 3. Use lazy state initialization for expensive values
- 核心问题
- 反例
- 推荐:lazy init
- 典型场景
- 一句话总结
- 4. Use derived state subscriptions
- 核心问题
- 反例
- 推荐:派生 state
- 在全局状态库中更重要
- 一句话总结
- 5. Apply startTransition for non-urgent updates
- 核心问题
- 典型场景
- 反例:同步更新
- 推荐:startTransition
- 什么时候该用 startTransition
- 总结
前言
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
functionuseUser(){const[data,setData]=useState(null)useEffect(()=>{fetch('/api/user').then(r=>r.json()).then(setData)},[])returndata}<Header/><Sidebar/>/api/user被请求两次
推荐:SWR 自动去重
importuseSWRfrom'swr'constfetcher=(url:string)=>fetch(url).then(r=>r.json())functionuseUser(){returnuseSWR('/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
反例:提前解构
const{user,theme,locale}=useAppStore()即使只用theme:
- user 更新
- locale 更新
组件都会重渲染。
推荐:用到再读
consttheme=useAppStore(state=>state.theme)Zustand / Redux / Jotai 都适用
useSelector(state=>state.user.name)一句话总结
订阅越小,重渲染越少
3. Use lazy state initialization for expensive values
「初始值贵,就别每次算」
核心问题
useState(expensiveCompute())- 每次 render 都会执行
expensiveCompute - 即使只用第一次
反例
const[value]=useState(buildBigMap(data))推荐:lazy init
const[value]=useState(()=>buildBigMap(data))函数只在初次 render 执行一次。
useState 的两种“初始化模式”:
普通初始化(每次 render 都会算),也就是
useState(buildBigMap(data)),React 在 render 前,先执行 buildBigMap,把执行结果作为参数传给 useState,每次 render 都会执行一次。Lazy 初始化,也就是
useState(() => buildBigMap(data)),传递给 useState 的是一个 Initializer function,内部处理如下
functionuseState(initialState){if(isFirstRender){if(typeofinitialState==='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”」
核心问题
const[filtered,setFiltered]=useState([])但filtered明明来自list + keyword
双源真相,必出 bug
反例
useEffect(()=>{setFiltered(list.filter(i=>i.name.includes(keyword)))},[list,keyword])推荐:派生 state
constfiltered=useMemo(()=>{returnlist.filter(i=>i.name.includes(keyword))},[list,keyword])在全局状态库中更重要
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 切换后加载数据
反例:同步更新
onChange={(e)=>{setKeyword(e.target.value)setFilteredData(filter(data,e.target.value))}}输入会卡。因为 React 内部的理解是:“这两个 setState 同等重要,必须立刻算完”,如果 filter(data) 很重:
- 输入法卡
- 光标延迟
- 掉帧
二、startTransition 到底做了什么?
推荐:startTransition
import{startTransition}from'react'onChange={(e)=>{setKeyword(e.target.value)startTransition(()=>{setFilteredData(filter(data,e.target.value))})}}效果:
- 输入优先
- 列表稍后更新
- UI 更丝滑
startTransition = 告诉 React:「这次更新不着急,别挡住用户操作」。没有 startTransition
用户输入 ↓ React:必须算完 filter(500ms) ↓UI更新使用 startTransition
用户输入 ↓ 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 性能心法
少订阅
少算
慢更新