渲染优化体系
React 性能优化的核心策略与实践
1. React 渲染机制
1.1 什么时候会重新渲染?
┌─────────────────────────────────────────────────────────────────┐
│ 触发重新渲染的情况 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 组件自身 state 变化 │
│ 2. 父组件重新渲染(即使 props 没变) │
│ 3. Context 值变化 │
│ 4. 自定义 Hook 中的 state 变化 │
│ │
└─────────────────────────────────────────────────────────────────┘1.2 重新渲染 ≠ DOM 更新
jsx
// 组件重新渲染(执行函数)
// ↓
// 生成新的 Virtual DOM
// ↓
// Diff 对比
// ↓
// 只有变化的部分更新真实 DOM重要:组件重新渲染本身开销不大,真正昂贵的是 DOM 操作。
2. React.memo
2.1 基本用法
jsx
// 只有 props 变化时才重新渲染
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* 复杂渲染 */}</div>;
});
// 自定义比较函数
const MemoizedComponent = memo(
function MyComponent({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示 props 相等,不需要重新渲染
return prevProps.user.id === nextProps.user.id;
}
);2.2 什么时候用 memo?
jsx
// ✅ 适合使用 memo 的场景
// 1. 组件渲染开销大
// 2. 父组件频繁重新渲染但 props 基本不变
// 3. 纯展示组件
// ❌ 不需要 memo 的场景
// 1. 组件渲染很快
// 2. props 经常变化
// 3. 父组件很少重新渲染2.3 memo 的陷阱
jsx
// ❌ 错误:每次渲染都创建新对象/函数
function Parent() {
return (
<MemoizedChild
style={{ color: 'red' }} // 每次都是新对象
onClick={() => {}} // 每次都是新函数
/>
);
}
// ✅ 正确:稳定的引用
function Parent() {
const style = useMemo(() => ({ color: 'red' }), []);
const handleClick = useCallback(() => {}, []);
return <MemoizedChild style={style} onClick={handleClick} />;
}3. useMemo 与 useCallback
3.1 useMemo - 缓存计算结果
jsx
function ProductList({ products, filter }) {
// 只有 products 或 filter 变化时才重新计算
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === filter);
}, [products, filter]);
return filteredProducts.map(p => <ProductCard key={p.id} {...p} />);
}3.2 useCallback - 缓存函数
jsx
function Parent() {
const [count, setCount] = useState(0);
// 不用 useCallback:每次渲染都是新函数
// const handleClick = () => { ... };
// 用 useCallback:函数引用稳定
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <MemoizedChild onClick={handleClick} />;
}3.3 什么时候需要?
jsx
// ✅ 需要 useMemo 的场景
// 1. 计算开销大(复杂过滤、排序、转换)
// 2. 结果作为其他 Hook 的依赖
// 3. 传递给 memo 组件的对象/数组
// ✅ 需要 useCallback 的场景
// 1. 传递给 memo 组件的回调函数
// 2. 作为其他 Hook 的依赖
// 3. 传递给需要稳定引用的第三方库
// ❌ 不需要的场景
// 1. 简单计算
// 2. 不作为依赖使用
// 3. 传递给普通(非 memo)组件4. 状态管理优化
4.1 状态下沉
jsx
// ❌ 状态放太高,导致整个组件树重新渲染
function App() {
const [inputValue, setInputValue] = useState('');
return (
<div>
<input value={inputValue} onChange={e => setInputValue(e.target.value)} />
<ExpensiveList /> {/* 被迫重新渲染 */}
</div>
);
}
// ✅ 状态下沉到需要的地方
function App() {
return (
<div>
<SearchInput /> {/* 状态在这里 */}
<ExpensiveList /> {/* 不会重新渲染 */}
</div>
);
}
function SearchInput() {
const [inputValue, setInputValue] = useState('');
return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />;
}4.2 组件拆分
jsx
// ❌ 大组件:任何状态变化都重新渲染整个组件
function Dashboard() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [data, setData] = useState(null);
return (
<div>
<Sidebar open={sidebarOpen} />
<ExpensiveChart data={data} /> {/* sidebarOpen 变化也会重新渲染 */}
</div>
);
}
// ✅ 拆分组件:隔离状态影响范围
function Dashboard() {
return (
<div>
<SidebarContainer /> {/* 自己管理 open 状态 */}
<ChartContainer /> {/* 自己管理 data 状态 */}
</div>
);
}4.3 Context 优化
jsx
// ❌ 任何值变化都导致所有消费者重新渲染
const AppContext = createContext({ user: null, theme: 'light', notifications: [] });
// ✅ 方案一:拆分 Context
const UserContext = createContext(null);
const ThemeContext = createContext('light');
// ✅ 方案二:useMemo 包装 value
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// ✅ 方案三:使用状态管理库(Zustand、Jotai)5. 虚拟列表
5.1 问题:大列表渲染
jsx
// ❌ 渲染 10000 个 DOM 节点
function BigList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // 10000 个 li
))}
</ul>
);
}5.2 虚拟列表原理
┌─────────────────────────────────────────────────────────────────┐
│ 虚拟列表原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ item 1 │ ← 不渲染(在视口上方) │
│ │ item 2 │ │
│ ├─────────┤ ─ 视口顶部 ───────────────────── │
│ │ item 3 │ ← 渲染 │
│ │ item 4 │ ← 渲染 │
│ │ item 5 │ ← 渲染 │
│ ├─────────┤ ─ 视口底部 ───────────────────── │
│ │ item 6 │ ← 不渲染(在视口下方) │
│ │ ... │ │
│ └─────────┘ │
│ │
│ 只渲染可视区域 + 上下缓冲区的元素 │
│ │
└─────────────────────────────────────────────────────────────────┘5.3 使用 react-window
jsx
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={400} // 容器高度
width="100%" // 容器宽度
itemCount={items.length}
itemSize={50} // 每项高度
>
{Row}
</FixedSizeList>
);
}5.4 动态高度列表
jsx
import { VariableSizeList } from 'react-window';
function DynamicList({ items }) {
const listRef = useRef();
const sizeMap = useRef({});
const setSize = useCallback((index, size) => {
sizeMap.current[index] = size;
listRef.current?.resetAfterIndex(index);
}, []);
const getSize = (index) => sizeMap.current[index] || 50;
return (
<VariableSizeList
ref={listRef}
height={400}
itemCount={items.length}
itemSize={getSize}
>
{({ index, style }) => (
<DynamicRow
data={items[index]}
style={style}
setSize={setSize}
index={index}
/>
)}
</VariableSizeList>
);
}6. React DevTools Profiler
6.1 使用 Profiler
jsx
import { Profiler } from 'react';
function App() {
const onRender = (id, phase, actualDuration) => {
console.log({ id, phase, actualDuration });
};
return (
<Profiler id="App" onRender={onRender}>
<MyComponent />
</Profiler>
);
}6.2 onRender 回调参数
javascript
function onRender(
id, // Profiler 的 id
phase, // "mount" 或 "update"
actualDuration, // 本次渲染耗时
baseDuration, // 无优化时预计耗时
startTime, // 开始渲染的时间
commitTime // 提交的时间
) {
// 记录性能数据
}6.3 React DevTools 使用
React DevTools → Profiler 面板
1. 点击录制按钮
2. 执行要分析的操作
3. 停止录制
4. 分析火焰图:
- 灰色:未重新渲染
- 黄色/橙色:渲染耗时
- 越亮表示耗时越长7. 其他优化技巧
7.1 延迟加载
jsx
// 组件懒加载
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}7.2 防抖与节流
jsx
function SearchInput() {
const [query, setQuery] = useState('');
// 防抖搜索
const debouncedSearch = useMemo(
() => debounce((value) => {
fetchResults(value);
}, 300),
[]
);
const handleChange = (e) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return <input value={query} onChange={handleChange} />;
}7.3 使用 key 重置组件
jsx
// 使用 key 强制重新挂载,比复杂的状态重置逻辑更清晰
<UserProfile key={userId} userId={userId} />8. 面试高频问题
Q1: React 性能优化有哪些方法?
- 组件优化:memo、useMemo、useCallback
- 状态优化:状态下沉、Context 拆分
- 渲染优化:虚拟列表、懒加载
- 避免不必要渲染:key 使用、稳定引用
Q2: useMemo 和 useCallback 的区别?
| 区别 | useMemo | useCallback |
|---|---|---|
| 返回值 | 缓存计算结果 | 缓存函数本身 |
| 执行 | 立即执行传入函数 | 不执行,返回函数 |
| 用途 | 计算值 | 回调函数 |
Q3: 为什么不给所有组件加 memo?
- 比较开销:memo 需要 shallow compare props
- 大多数组件渲染很快:优化收益不大
- 增加复杂度:代码更难理解
- React Compiler:未来会自动优化
Q4: 如何定位性能问题?
- React DevTools Profiler:分析组件渲染耗时
- Chrome DevTools Performance:分析整体性能
- why-did-you-render:检测不必要的重新渲染
- console.log 渲染次数:简单但有效
Q5: 虚拟列表的原理?
只渲染可视区域内的元素,通过监听滚动事件动态计算应该渲染哪些元素,使用绝对定位或 transform 放置元素,实现"滚动"效果。