React 性能优化实战:重渲染分析与列表优化

性能问题往往不是凭感觉解决的,先搞清楚「是谁在频繁重渲染」「一次渲染里干了哪些重活」,再决定怎么下手。

最直接的工具是 React DevTools 的 Profiler。

常见几类现象:

  • 某个父组件状态一变,下面一大片子组件全部跟着渲染;
  • 一些列表项组件,在数据没变的情况下也频繁重渲染;
  • 某个组件单次渲染耗时明显偏高。

如果在 DevTools 里把「highlight updates」打开,肉眼也能看到哪些块在不停闪。

最常见的一类问题:父组件每次渲染都会重新创建 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,可以避免不必要的卸载/重建:

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 用对,再根据规模选择虚拟列表或分块渲染。
  • 把重计算从渲染路径里挪出去,必要时配合并发特性,让交互优先,重渲染慢一步。