React 性能优化实战:重渲染分析与列表优化
性能问题往往不是凭感觉解决的,先搞清楚「是谁在频繁重渲染」「一次渲染里干了哪些重活」,再决定怎么下手。
先看清楚谁在重渲染
最直接的工具是 React DevTools 的 Profiler。
常见几类现象:
- 某个父组件状态一变,下面一大片子组件全部跟着渲染;
- 一些列表项组件,在数据没变的情况下也频繁重渲染;
- 某个组件单次渲染耗时明显偏高。
如果在 DevTools 里把「highlight updates」打开,肉眼也能看到哪些块在不停闪。
减少不必要的重渲染
用 React.memo 缓住稳定的子组件
最常见的一类问题:父组件每次渲染都会重新创建 props(例如函数/对象字面量),导致子组件跟着重渲染。
1const Item = React.memo(function Item({ value, onClick }: {
2 value: number;
3 onClick: () => void;
4}) {
5 console.log('render item', value);
6 return <li onClick={onClick}>{value}</li>;
7});
配合 useCallback 稳定回调引用,可以让「实际 props 没变」的子组件跳过渲染:
1function List({ items }: { items: number[] }) {
2 const handleClick = useCallback((value: number) => {
3 console.log('click', value);
4 }, []);
5
6 return (
7 <ul>
8 {items.map(item => (
9 <Item
10 key={item}
11 value={item}
12 onClick={() => handleClick(item)}
13 />
14 ))}
15 </ul>
16 );
17}
这里的箭头函数仍然是每次新建的,但 Item 真正关心的是内部调用的那个 handleClick,它已经通过 useCallback 稳定下来了。
更进一步可以把事件绑定也抽出去,按需要权衡代码复杂度。
拆分状态,避免「一变全变」
如果一个组件同时管理很多互不相关的状态,任何一项变化都会触发整块重渲染。
可以考虑:
- 按功能拆成多个子组件,各自只订阅自己关心的状态;
- 或者用多个 Context 拆开,而不是一个大 Context 装所有东西。
简单的思路是:尽量让每个组件只对必要的那部分状态敏感。
列表优化:key、虚拟化和按需渲染
正确使用 key
在列表中使用稳定且唯一的 key,可以避免不必要的卸载/重建:
1{items.map(item => (
2 <Row key={item.id} item={item} />
3))}
避免使用索引作为 key,尤其是在会插入/删除中间元素的列表里,否则会导致:
- DOM 复用错位;
- 输入框光标跳动;
- 动画和状态对不齐。
虚拟列表:只渲染可见部分
当列表很长(几千上万行)时,即便单个项渲染很轻,累积起来也会让首屏和滚动变得吃力。
这时可以考虑虚拟列表(窗口化),例如:
react-window;react-virtualized;- 或者自己实现一个简单版:根据滚动位置只渲染可见范围内的若干行。
核心思路是:DOM 节点的数量控制在一个合理上限内,滚动时只更新那一小撮可见项。
分块渲染与懒加载
对于一次性渲染大量内容的场景,可以考虑:
- 把列表拆成多个「块」,用
setTimeout/requestIdleCallback等方式逐步渲染; - 或者结合
startTransition,把不那么着急的那一批渲染降个优先级。
这类方案本质上是在和浏览器「抢时间片」,让每一帧内的工作量控制在可接受范围之内。
避免在渲染函数里做重活
常见的重活包括:
- 复杂的计算(大数组的 filter/sort/map、多层嵌套循环);
- 庞大的对象结构构建;
- 直接操作 DOM 或调用代价高的 API。
这些东西如果必须做,可以尝试:
- 用
useMemo缓存计算结果,让它只在依赖变化时重算; - 把计算提前到更上层,甚至挪到 worker 里;
- 延后到
useEffect,减少对首次渲染时间的影响。
例子:
1const visibleItems = useMemo(() => {
2 // 假设 items 很大,筛选和排序都比较重
3 return items
4 .filter(matchKeyword(keyword))
5 .sort(customComparator);
6}, [items, keyword]);
前提依然是:这个计算本身确实够重,否则多一层记忆带来的收益未必明显。
和并发特性的配合
在 React 18 的并发能力下,除了减少工作量,还可以主动标记「不着急的更新」:
- 输入与交互相关的状态用高优先级(普通 setState);
- 会触发大量渲染的筛选、列表更新等,可以放在
startTransition/useTransition里; - 用
useDeferredValue让重组件「慢半拍」,保持交互区域流畅。
这类手段不会凭空提升计算速度,但能够让「体感卡顿」减轻不少。
小结
- 性能优化的入口是「先量后治」,用 Profiler 看清楚是谁在频繁重渲染。
- 用
React.memo+useCallback/useMemo时,重点是找出真正贵的部分,而不是全局套一层。 - 列表场景下,要先把 key 用对,再根据规模选择虚拟列表或分块渲染。
- 把重计算从渲染路径里挪出去,必要时配合并发特性,让交互优先,重渲染慢一步。