React性能的正确思维模型
大多数React性能问题都是架构问题,而非框架限制。在求助于useMemo或React.memo之前,先理解React为何重新渲染。
React在以下情况下会重新渲染组件:
- 其状态发生变化(
useState、useReducer) - 其父组件重新渲染并传递了新props
- 其订阅的context发生变化
目标不是消除重新渲染,而是消除不必要的重新渲染,并确保必要的重新渲染足够快。

先测量再优化
import { Profiler, ProfilerOnRenderCallback } from 'react'
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
) => {
if (actualDuration > 16) {
console.warn(`Slow render: ${id} took ${actualDuration.toFixed(2)}ms`)
}
}
function App() {
return (
<Profiler id="ProductList" onRender={onRender}>
<ProductList />
</Profiler>
)
}
打开React DevTools Profiler,记录一次交互,并查找:
- 渲染频率高于预期的组件
- 渲染时长超过16ms(60fps下的一帧)的组件
React.memo:何时有用
React.memo阻止组件在其props未变化时重新渲染:
// 不使用memo:每次父组件重新渲染时都会重新渲染
function ExpensiveChart({ data, title }: ChartProps) { ... }
// 使用memo:仅在data或title实际变化时重新渲染
const ExpensiveChart = React.memo(function ExpensiveChart({ data, title }: ChartProps) { ... })
引用相等陷阱:
// 错误:memo无效——每次渲染时options都是新对象
function Parent() {
return <Chart options={{ color: 'blue', width: 300 }} />
}
// 正确:将常量对象移到组件外部
const CHART_OPTIONS = { color: 'blue', width: 300 }
function Parent() {
return <Chart options={CHART_OPTIONS} />
}
useMemo:缓存昂贵计算
import { useMemo } from 'react'
function ProductList({ products, filters, sortBy }: Props) {
// 错误:每次渲染都重新计算,即使是不相关的状态变化
const filteredProducts = products
.filter(p => filters.every(f => f(p)))
.sort((a, b) => compareFn(a, b, sortBy))
// 正确:仅在输入变化时重新计算
const filteredProducts = useMemo(
() => products
.filter(p => filters.every(f => f(p)))
.sort((a, b) => compareFn(a, b, sortBy)),
[products, filters, sortBy]
)
return <ul>{filteredProducts.map(p => <ProductItem key={p.id} product={p} />)}</ul>
}
稳定的context值:
function UserProfile({ userId }: Props) {
const [theme, setTheme] = useState('light')
// 错误:每次渲染都创建新的context值对象,所有消费者都重新渲染
// return <UserContext.Provider value={{ userId, theme, setTheme }}>
// 正确:仅在userId或theme变化时创建新对象
const contextValue = useMemo(
() => ({ userId, theme, setTheme }),
[userId, theme]
)
return (
<UserContext.Provider value={contextValue}>
<UserDetails />
</UserContext.Provider>
)
}
useCallback:稳定的函数引用
import { useCallback, memo } from 'react'
const ExpensiveChild = memo(function ExpensiveChild({
onAction,
}: {
onAction: (id: string) => void
}) {
return <div>...</div>
})
function Parent() {
const [count, setCount] = useState(0)
// 错误:每次渲染都创建新的函数引用——破坏了ExpensiveChild上的memo
// const handleAction = (id: string) => { ... }
// 正确:稳定的引用——ExpensiveChild上的memo按预期工作
const handleAction = useCallback((id: string) => {
analytics.track('action', { id })
}, [])
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild onAction={handleAction} />
</>
)
}
规则:useCallback主要用于将回调传递给已记忆的子组件。
useTransition:保持UI响应
React Concurrent Mode允许你将状态更新标记为非紧急。UI在处理后台昂贵更新时保持响应:
import { useState, useTransition } from 'react'
function SearchPage() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const [results, setResults] = useState<SearchResult[]>([])
const handleSearch = (value: string) => {
// 立即:更新输入(紧急——用户正在输入)
setQuery(value)
// 延迟:执行昂贵搜索(非紧急——可以等待)
startTransition(() => {
const newResults = searchIndex.query(value)
setResults(newResults)
})
}
return (
<div>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
)
}
useDeferredValue——当你无法控制状态更新时:
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
const results = useMemo(() => searchIndex.query(deferredQuery), [deferredQuery])
return (
<div style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}>
{results.map(r => <ResultItem key={r.id} result={r} />)}
</div>
)
}
虚拟列表:处理大量数据
渲染10,000个列表项会导致浏览器崩溃。虚拟列表只渲染可见部分:
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
function VirtualUserList({ users }: { users: User[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: users.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 72,
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<UserRow user={users[virtualRow.index]} />
</div>
))}
</div>
</div>
)
}
使用虚拟化,渲染100,000个项与渲染20个一样快。
状态共置:架构修复
将状态提升过高会导致过多重新渲染:
// 错误:App中的搜索状态在每次按键时重新渲染整个树
function App() {
const [searchQuery, setSearchQuery] = useState('')
return (
<div>
<Header /> {/* 不必要地重新渲染 */}
<Sidebar /> {/* 不必要地重新渲染 */}
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<SearchResults query={searchQuery} />
</div>
)
}
// 正确:将状态与需要它的组件共置
function App() {
return (
<div>
<Header />
<Sidebar />
<SearchSection /> {/* 包含自己的状态 */}
</div>
)
}
function SearchSection() {
const [searchQuery, setSearchQuery] = useState('')
return (
<>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<SearchResults query={searchQuery} />
</>
)
}
Context性能陷阱
// 错误:当user或theme变化时,每个消费者都重新渲染
const AppContext = createContext<AppState>(defaultState)
// 正确:拆分context——user消费者不会因theme变化而重新渲染
const UserContext = createContext<UserContextValue>(defaultUser)
const ThemeContext = createContext<ThemeContextValue>(defaultTheme)
性能检查清单
- 先分析——在React DevTools Profiler中识别慢组件
- 检查渲染频率——是否比预期更频繁地渲染?
- 检查渲染时长——此渲染本身是否缓慢?
- 对于高频渲染:使用
React.memo+useCallback/useMemo保持稳定引用 - 对于慢渲染:虚拟化长列表,使用
useMemo进行昂贵计算,使用useTransition处理非紧急更新 - 对于架构问题:共置状态,拆分context
先测量,识别具体瓶颈,然后应用针对性修复。