文章目录
- 前言
- Guidelines
- Medium-Impact
-
- [1. Use SWR for automatic request deduplication](#1. Use SWR for automatic request deduplication)
- [2. Defer state reads to usage point](#2. Defer state reads to usage point)
- [3. Use lazy state initialization for expensive values](#3. Use lazy state initialization for expensive values)
- [4. Use derived state subscriptions](#4. Use derived state subscriptions)
-
- 核心问题
- 反例
- [推荐:派生 state](#推荐:派生 state)
- 在全局状态库中更重要
- 一句话总结
- [5. Apply startTransition for non-urgent updates](#5. Apply startTransition for non-urgent updates)
-
- 核心问题
- 典型场景
- 反例:同步更新
- 推荐:startTransition
- [什么时候该用 startTransition](#什么时候该用 startTransition)
- 总结
前言
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 的两种"初始化模式":
-
普通初始化(每次 render 都会算),也就是
useState(buildBigMap(data)),React 在 render 前,先执行 buildBigMap,把执行结果作为参数传给 useState,每次 render 都会执行一次。 -
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) 很重:
- 输入法卡
- 光标延迟
- 掉帧
二、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 性能心法
少订阅
少算
慢更新