React 性能优化
React 应用的性能优化涉及减少不必要的渲染、优化组件结构和合理使用缓存。
理解 React 渲染机制
何时触发重新渲染
- State 变化:组件自身 state 改变
- Props 变化:父组件传递的 props 改变
- Context 变化:订阅的 Context 值改变
- 父组件重新渲染:父组件渲染会触发所有子组件渲染
jsx
function Parent() {
const [count, setCount] = useState(0)
console.log('Parent render')
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* 即使 Child 的 props 没变,也会重新渲染 */}
<Child name="Tom" />
</div>
)
}
function Child({ name }) {
console.log('Child render') // 每次点击都会打印
return <p>Hello, {name}</p>
}React.memo
React.memo 是高阶组件,用于缓存组件,只在 props 变化时重新渲染。
基础用法
jsx
const Child = React.memo(function Child({ name }) {
console.log('Child render')
return <p>Hello, {name}</p>
})
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* 现在只有 name 变化时才重新渲染 */}
<Child name="Tom" />
</div>
)
}自定义比较函数
jsx
const UserCard = React.memo(
function UserCard({ user, onClick }) {
console.log('UserCard render')
return (
<div onClick={onClick}>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
)
},
// 自定义比较函数
(prevProps, nextProps) => {
// 返回 true 表示不需要重新渲染
return prevProps.user.id === nextProps.user.id
}
)memo 失效的情况
jsx
function Parent() {
const [count, setCount] = useState(0)
// ❌ 每次渲染都创建新对象,memo 失效
const user = { name: 'Tom' }
// ❌ 每次渲染都创建新函数,memo 失效
const handleClick = () => console.log('clicked')
return (
<Child user={user} onClick={handleClick} />
)
}
// ✅ 解决方案:使用 useMemo 和 useCallback
function Parent() {
const [count, setCount] = useState(0)
const user = useMemo(() => ({ name: 'Tom' }), [])
const handleClick = useCallback(() => console.log('clicked'), [])
return (
<Child user={user} onClick={handleClick} />
)
}useMemo
useMemo 用于缓存计算结果,只在依赖变化时重新计算。
缓存计算值
jsx
function ProductList({ products, filter }) {
// ✅ 只在 products 或 filter 变化时重新计算
const filteredProducts = useMemo(() => {
console.log('Filtering products...')
return products.filter(p => p.category === filter)
}, [products, filter])
return (
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}缓存引用值
jsx
function Parent() {
const [count, setCount] = useState(0)
// ✅ 缓存对象引用,避免子组件不必要的渲染
const config = useMemo(() => ({
theme: 'dark',
language: 'zh'
}), [])
// ✅ 缓存数组
const items = useMemo(() => [1, 2, 3, 4, 5], [])
return <Child config={config} items={items} />
}何时使用 useMemo
jsx
// ✅ 适合使用 useMemo
// 1. 计算开销大
const sortedList = useMemo(() => {
return [...list].sort((a, b) => a.value - b.value)
}, [list])
// 2. 作为其他 Hook 的依赖
const memoizedValue = useMemo(() => ({ id: 1 }), [])
useEffect(() => {
// 使用 memoizedValue
}, [memoizedValue])
// 3. 传递给 memo 包裹的子组件
const Child = React.memo(({ config }) => <div>{config.name}</div>)
const config = useMemo(() => ({ name: 'test' }), [])
<Child config={config} />
// ❌ 不需要使用 useMemo
// 1. 简单计算
const double = count * 2 // 直接计算即可
// 2. 原始值
const name = 'Tom' // 原始值不需要缓存useCallback
useCallback 用于缓存函数引用,只在依赖变化时创建新函数。
基础用法
jsx
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// ✅ 函数引用稳定,不会导致 Child 重新渲染
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
// ✅ 依赖 count,count 变化时创建新函数
const handleIncrement = useCallback(() => {
setCount(c => c + 1)
}, []) // 使用函数式更新,不需要依赖 count
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<Child onClick={handleClick} />
</div>
)
}
const Child = React.memo(({ onClick }) => {
console.log('Child render')
return <button onClick={onClick}>Click</button>
})使用场景
jsx
// 1. 传递给 memo 组件
const MemoChild = React.memo(({ onSubmit }) => { ... })
function Parent() {
const handleSubmit = useCallback((data) => {
submitData(data)
}, [])
return <MemoChild onSubmit={handleSubmit} />
}
// 2. 作为 useEffect 依赖
function SearchResults({ query }) {
const fetchResults = useCallback(async () => {
const data = await api.search(query)
setResults(data)
}, [query])
useEffect(() => {
fetchResults()
}, [fetchResults])
}
// 3. 自定义 Hook 中
function useDebounce(callback, delay) {
const callbackRef = useRef(callback)
callbackRef.current = callback
return useCallback((...args) => {
// 使用 ref 避免依赖 callback
setTimeout(() => callbackRef.current(...args), delay)
}, [delay])
}懒加载
React.lazy
jsx
import { lazy, Suspense } from 'react'
// 懒加载组件
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
}命名导出的懒加载
jsx
// utils.js
export const HeavyComponent = () => { ... }
// 懒加载命名导出
const HeavyComponent = lazy(() =>
import('./utils').then(module => ({
default: module.HeavyComponent
}))
)预加载
jsx
const Dashboard = lazy(() => import('./Dashboard'))
// 预加载函数
const preloadDashboard = () => import('./Dashboard')
function Sidebar() {
return (
<nav>
{/* 鼠标悬停时预加载 */}
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
>
Dashboard
</Link>
</nav>
)
}虚拟列表
对于长列表,只渲染可视区域内的元素:
使用 react-window
jsx
import { FixedSizeList as List } from 'react-window'
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)
return (
<List
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
)
}使用 react-virtualized
jsx
import { List, AutoSizer } from 'react-virtualized'
function VirtualList({ items }) {
const rowRenderer = ({ index, key, style }) => (
<div key={key} style={style}>
{items[index].name}
</div>
)
return (
<AutoSizer>
{({ height, width }) => (
<List
height={height}
width={width}
rowCount={items.length}
rowHeight={50}
rowRenderer={rowRenderer}
/>
)}
</AutoSizer>
)
}状态优化
状态下沉
将状态放到需要它的最小组件中:
jsx
// ❌ 状态提升过高,导致整个 App 重新渲染
function App() {
const [inputValue, setInputValue] = useState('')
return (
<div>
<Header />
<Sidebar />
<input
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
<Content />
</div>
)
}
// ✅ 状态下沉到需要的组件
function App() {
return (
<div>
<Header />
<Sidebar />
<SearchInput /> {/* 状态在这里管理 */}
<Content />
</div>
)
}
function SearchInput() {
const [inputValue, setInputValue] = useState('')
return (
<input
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
)
}拆分 Context
jsx
// ❌ 单一大 Context,任何值变化都触发所有消费者更新
const AppContext = createContext()
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const [notifications, setNotifications] = useState([])
return (
<AppContext.Provider value={{
user, setUser,
theme, setTheme,
notifications, setNotifications
}}>
{children}
</AppContext.Provider>
)
}
// ✅ 拆分为多个 Context
const UserContext = createContext()
const ThemeContext = createContext()
const NotificationContext = createContext()
function AppProvider({ children }) {
return (
<UserProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</UserProvider>
)
}避免不必要的渲染
使用 children 模式
jsx
// ❌ ExpensiveComponent 会随 Parent 状态变化而重新渲染
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveComponent />
</div>
)
}
// ✅ 将 ExpensiveComponent 作为 children 传入
function Parent({ children }) {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{children}
</div>
)
}
function App() {
return (
<Parent>
<ExpensiveComponent /> {/* 不会重新渲染 */}
</Parent>
)
}使用 key 重置组件
jsx
function UserProfile({ userId }) {
// 使用 key 在 userId 变化时重置组件状态
return <Profile key={userId} userId={userId} />
}
function Profile({ userId }) {
const [isEditing, setIsEditing] = useState(false)
// userId 变化时,整个组件重新挂载,isEditing 重置为 false
return <div>...</div>
}性能分析工具
React DevTools Profiler
jsx
// 使用 Profiler 组件测量渲染性能
import { Profiler } from 'react'
function onRenderCallback(
id, // Profiler 的 id
phase, // "mount" | "update"
actualDuration, // 本次渲染耗时
baseDuration, // 不使用 memo 时的预估耗时
startTime, // 开始渲染的时间
commitTime // 提交更新的时间
) {
console.log({ id, phase, actualDuration })
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Main />
</Profiler>
)
}使用 why-did-you-render
javascript
// wdyr.js
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true
})
}
// 在入口文件顶部引入
import './wdyr'常见面试题
1. React.memo、useMemo、useCallback 的区别?
React.memo: 缓存组件,props 不变则不重新渲染useMemo: 缓存计算结果useCallback: 缓存函数引用
2. 什么时候使用 useMemo?
- 计算开销大的操作
- 作为其他 Hook 的依赖
- 传递给 memo 组件的对象/数组
3. 为什么函数作为 props 会导致子组件重新渲染?
每次父组件渲染都会创建新的函数对象,引用不同,即使函数内容相同。使用 useCallback 可以保持函数引用稳定。
4. 如何优化长列表渲染?
- 使用虚拟列表(react-window、react-virtualized)
- 分页加载
- 使用 key 帮助 React 识别列表项