人人都会AI编程

21.2 列表性能优化

更新时间:2026-07-11

列表是 React 应用中最常见的 UI 形态之一:商品列表、聊天记录、表格数据、通知消息……当列表项达到成百上千条时,如果不做任何优化,页面就会出现明显的性能问题:首次渲染白屏时间长,滚动时掉帧卡顿。本节介绍两种最核心的列表性能优化手段:虚拟列表key 属性的正确使用


21.2.1 虚拟列表:只渲染看得见的内容

问题根源

假设一个列表有 10000 条数据,每条数据渲染为一个包含头像、文字、按钮的复杂组件。React 会创建 10000 个 DOM 节点,执行 10000 次组件渲染。这不仅导致初始渲染极其缓慢,滚动时浏览器还需要不断处理大量不可见 DOM 的布局和绘制,极易出现卡顿。

解决方案:窗口化渲染

虚拟列表(Virtual List,也称窗口化 Windowing)的思路很简单:只渲染当前可视区域及其周边少量缓冲区内的列表项,其余部分用占位元素撑开高度。当用户滚动时,动态替换可视区域的项,始终保持实际渲染的 DOM 数量在一个极小的范围内(通常 10~50 个),滚动性能与列表总长度无关。

推荐库:react-window

react-window 是目前最轻量、高性能的虚拟列表库(仅 2KB gzip),由 react-virtualized 的作者重写,API 更简洁。绝大多数场景下应该优先选择它。

安装

npm install react-window

固定高度列表

import { FixedSizeList as List } from 'react-window';

function Row({ index, style, data }) {
  const item = data[index];
  return (
    <div style={style}>
      {item.name} - {item.email}
    </div>
  );
}

function VirtualizedList({ items }) {
  return (
    <List
      height={400}          // 列表可视区域高度
      itemCount={items.length}
      itemSize={50}         // 每项高度(固定)
      width="100%"
      itemData={items}      // 传递给每行的数据
    >
      {Row}
    </List>
  );
}

FixedSizeList 要求每项高度一致。style 参数必须应用到根元素上,它包含了绝对定位和变换属性,用于将行放置在正确的位置。

动态高度列表

当列表项高度不固定(如包含不同长度文本)时,使用 VariableSizeList,需要提供估算高度和实际测量高度的方法。

import { VariableSizeList as List } from 'react-window';

function getItemSize(index) {
  // 根据数据估算每项高度,接近真实值可减少抖动
  return index % 2 === 0 ? 60 : 80;
}

function Row({ index, style, data }) {
  const item = data[index];
  return (
    <div style={style}>
      <strong>{item.title}</strong>
      <p>{item.content}</p>
    </div>
  );
}

function DynamicList({ items }) {
  const listRef = useRef();

  return (
    <List
      ref={listRef}
      height={400}
      itemCount={items.length}
      itemSize={getItemSize}  // 函数形式
      width="100%"
      itemData={items}
    >
      {Row}
    </List>
  );
}

对于高度完全无法预知的场景,可以使用 react-window 搭配 AutoSizer 自动获取容器宽度,以及 useRef + resetAfterIndex 进行动态高度重置。但建议尽可能让列表项高度可预测,性能才最稳定。

虚拟列表的配套设施

  • React.memo:列表项组件务必包裹 React.memo,避免父组件状态变化导致已渲染项的无意义重渲染。
  const Row = React.memo(({ index, style, data }) => {
    // ...
  });
  
  • useCallback 稳定回调:如果列表项内部需要处理点击事件,父组件中使用 useCallback 包裹回调函数,避免每次渲染生成新函数引用导致子组件重渲染。
  • 加载更多:虚拟列表通常配合滚动到底部加载更多数据,react-window 提供了 onItemsRendered 回调来判断当前渲染的范围,实现触底加载:
  <List
    onItemsRendered={({ overscanStartIndex, overscanStopIndex }) => {
      if (overscanStopIndex >= items.length - 1) {
        loadMore();
      }
    }}
  />
  

何时使用虚拟列表

  • 数据量 > 200 条,且列表项包含图片、复杂节点时,虚拟列表效果立竿见影。
  • 移动端或性能敏感场景,数据量 > 100 条就应启用。
  • 简单文本列表在 500 条以内可能影响不大,但仍推荐养成习惯。

21.2.2 key 属性的正确使用

key 的作用

React 在协调(Reconciliation)阶段,通过比较新旧虚拟 DOM 树来决定如何更新真实 DOM。对于列表,React 依赖每一项的 key 来判断元素的身份:相同 key 认为是同一个元素,触发移动或更新;不同 key 则认为是新增或删除

如果 key 缺失或选择不当,React 可能做出错误的更新决策,导致性能低效甚至状态错乱。

错误用法

1. 使用索引作为 key

{items.map((item, index) => (
  <ListItem key={index} data={item} />
))}

问题:当列表顺序改变(排序、插入、删除)时,索引发生变化。原本第一项的数据变了,但 key 仍然是 0,React 会认为这个元素只是内容更新了,而不会重新创建,这会导致:

  • 状态错乱:如果 ListItem 内部有非受控状态(如输入框内容),数据错位,输入框内容“粘”在位置上。
  • 性能浪费:无法利用元素的移动复用,每次都要更新节点。

2. 使用随机数作为 key

{items.map(item => (
  <ListItem key={Math.random()} data={item} />
))}

每次渲染都会生成新的 key,React 会认为所有元素都是新创建的,完全销毁旧 DOM 并创建新 DOM,性能极差,且丢失组件内部状态。

3. key 不唯一

{items.map(item => (
  <ListItem key={item.type} data={item} /> // 如果有多个相同 type 会冲突
))}

React 要求兄弟节点间 key 必须唯一,重复 key 会导致渲染错误。

正确用法

始终使用稳定、唯一的数据 ID 作为 key

{products.map(product => (
  <ProductCard key={product.id} product={product} />
))}

如果后端数据没有唯一 ID,可以在获取数据时生成(但必须保证每次数据相同项生成的 ID 一致),或使用合适的业务字段组合(如 ${item.name}_${item.timestamp})。

key 必须绑定在数组的直接元素上

key 应该写在 map 返回的最外层元素上,而不是该元素内部的某个子元素上。

真实案例:输入框错乱

一个常见的踩坑场景:待办事项列表,每一项包含一个 input 用于修改标题。使用索引作为 key 时,在头部插入新项目,会导致所有已有项的输入框内容全部错位——第一个输入框显示的内容变成了第二项的。改用唯一 ID 后立即恢复正确。

// ❌ 使用 index 作为 key,插入新项后输入框内容错乱
{items.map((item, index) => (
  <div key={index}>
    <input defaultValue={item.title} />
  </div>
))}

// ✅ 使用唯一 id
{items.map(item => (
  <div key={item.id}>
    <input defaultValue={item.title} />
  </div>
))}

总结:key 选择清单

  • 有唯一 ID 就用 ID。
  • 没有 ID 可以组合多个字段确保唯一性。
  • 万不得已使用索引时,必须确保列表不会发生重排序、插入或删除操作(静态列表)。
  • 绝对不要使用随机数、时间戳等不稳定值。

列表性能优化是前台应用中提升用户体验最直接的途径。掌握虚拟列表和正确的 key 用法,能让你轻松驾驭万级数据的流畅滚动,同时避免难以排查的状态错乱问题。