创见博客
React之虚拟列表
七崽爱吃小饼干2026/01/18阅读 2专栏 React

React 虚拟列表

一、什么是 React 虚拟列表(Virtual List)

React 虚拟列表(也叫「虚拟滚动」Virtual Scrolling),是一种高性能的长列表渲染优化技术。

它的核心定义:只渲染当前视口(浏览器可见区域)内的列表元素,同时复用 DOM 节点,对视口外的列表项进行「卸载/不渲染」处理,无论列表有1万条还是100万条数据,页面中实际存在的 DOM 节点数量始终保持在一个很小的固定范围内。

二、为什么需要虚拟列表?核心痛点(原生渲染的问题)

这是虚拟列表的核心价值,也是必须掌握的前置知识,所有优化都是围绕这个痛点展开:

原生直接渲染长列表的致命问题

当我们在 React 中直接用 map 渲染超长列表(比如1000+条数据)时:

jsx
// 错误示范:长列表直接渲染,性能爆炸
const LongList = ({ data }) => {
  return <div className="list-container">{data.map(item => <Item key={item.id} data={item} />)}</div>
}

会触发三大性能灾难:

  1. DOM 节点暴增:1000条数据就生成1000个 DOM 节点,浏览器的 DOM 解析、渲染压力剧增;
  2. 首次渲染速度极慢:React 要创建上千个虚拟DOM、进行Diff、最终挂载真实DOM,页面会出现明显的白屏/卡顿;
  3. 滚动时严重卡顿:滚动列表时,浏览器需要频繁重排(reflow)、重绘(repaint)大量DOM节点,帧率暴跌,用户体验极差。

虚拟列表的核心优势

完美解决上述问题,核心优势总结:

  • 无论列表数据量多大,页面中真实DOM节点数量恒定(一般只有20~50个);
  • 首次渲染速度提升10倍+,页面秒开无白屏;
  • 滚动过程丝滑流畅,不会出现任何卡顿;
  • 极大降低内存占用,避免因DOM节点过多导致的内存泄漏风险。

三、虚拟列表的核心实现原理

虚拟列表的原理说起来不复杂,所有实现方案的底层逻辑都是这4个核心点,万变不离其宗,理解了这4点,你自己都能手写虚拟列表:

核心原理1:固定列表项的高度(基础前提)

虚拟列表的基础实现方案(也是最常用) 要求:列表中每一项(Item)的高度是固定值(比如 50px)。

补充:如果是「不定高列表项」,属于进阶方案,后面会讲优化思路,先掌握定高方案,90%的业务场景够用。

核心原理2:计算「可视区域能展示的列表项数量」

通过两个固定值计算一个核心数字,这个数字是固定不变的:

可视区域可展示数量 = Math.ceil(视口容器高度 / 单个列表项高度) + 2
  • 视口容器高度:列表的外层滚动容器的高度(比如 500px)
  • 单个列表项高度:固定值(比如 50px)
  • 加2的目的:渲染「缓冲区」,滚动时不会出现「白屏闪烁」,让滚动更顺滑,是行业通用优化技巧。

举例:视口高500px,项高50px → 可展示10项,加2后实际渲染12项,DOM节点永远只有12个。

核心原理3:利用「滚动偏移量」计算「起始渲染索引」

列表外层容器开启滚动后,会产生滚动偏移量(scrollTop) —— 容器向上滚动的距离。 通过这个偏移量,能精准计算出「当前视口内,第一个需要渲染的列表项的索引」:

起始渲染索引 = Math.floor(滚动偏移量(scrollTop) / 单个列表项高度)

举例:项高50px,滚动了220px → 起始索引 = Math.floor(220/50) = 4 → 从第5条数据开始渲染。

核心原理4:计算「结束渲染索引」+ 核心技巧「空白占位区」

① 结束渲染索引

由「起始索引」和「可视区域可展示数量」直接得出:

结束渲染索引 = 起始渲染索引 + 可视区域可展示数量

我们只需要渲染「起始索引 ~ 结束索引」之间的列表项即可,这就是视口内的全部内容。

② 最关键的核心技巧:空白占位区(paddingTop / marginTop)

这是虚拟列表的灵魂,也是新手最容易忽略的点!

  • 问题:我们只渲染了视口内的10几条数据,那「滚动偏移量对应的、视口上方的那些数据」去哪里了?页面会没有滚动条,也无法滚动。
  • 解决方案:给列表的容器设置一个 动态的空白占位高度(用 paddingTop 实现最佳),这个高度等于「起始索引 × 单个列表项高度」。
  • 本质:这个空白占位区,模拟了「视口上方未渲染的所有列表项的总高度」,让浏览器认为列表有完整的高度,从而生成正常的滚动条,滚动行为也完全和原生长列表一致。

公式:

空白占位高度 = 起始渲染索引 × 单个列表项高度

四、虚拟列表的必备前置条件 & 核心属性/API

必备前置条件

  1. 列表的外层容器必须是固定高度,且设置 overflow: auto(开启滚动);
  2. 列表的每一项(Item)必须是固定高度(定高方案)。

核心DOM属性 & React事件

  1. scrollTop:获取滚动容器的垂直滚动偏移量(向上滚动的距离),只读属性;
  2. scrollHeight:获取滚动容器的总滚动高度(容器内所有内容的高度,包含未可视区域),只读属性;
  3. clientHeight:获取滚动容器的可视区域高度(容器本身的高度),只读属性;
  4. onScroll:React的滚动监听事件,绑定在滚动容器上,滚动时实时触发,用于计算上述核心参数。

五、手写 React 基础版虚拟列表

需求说明

实现一个可滚动的长列表,数据量10000条,每个列表项高度50px,列表容器高度500px,只渲染视口内的元素,滚动丝滑无卡顿。

完整代码

jsx
import { useState, useRef, useEffect } from 'react';

// 单个列表项组件 - 固定高度50px
const ListItem = ({ index, data }) => {
  return (
    <div 
      className="list-item" 
      style={{ height: '50px', lineHeight: '50px', borderBottom: '1px solid #eee', padding: '0 10px' }}
    >
      第{index + 1}条数据 - {data}
    </div>
  );
};

// 核心:虚拟列表组件
const VirtualList = ({ dataList }) => {
  // 1. 核心常量配置(可抽离成props,灵活配置)
  const ITEM_HEIGHT = 50; // 单个列表项固定高度
  const VIEWPORT_HEIGHT = 500; // 列表容器固定高度
  // 可视区域可展示数量 + 2个缓冲区,避免滚动闪烁
  const VISIBLE_COUNT = Math.ceil(VIEWPORT_HEIGHT / ITEM_HEIGHT) + 2;

  // 2. 状态管理:存储当前要渲染的列表数据
  const [renderList, setRenderList] = useState([]);
  // 3. 容器Ref:获取真实DOM的scrollTop/clientHeight等属性
  const listContainerRef = useRef(null);
  // 4. 状态管理:存储空白占位高度(paddingTop)
  const [paddingTop, setPaddingTop] = useState(0);

  // 核心方法:处理滚动事件,计算所有关键参数
  const handleScroll = () => {
    const container = listContainerRef.current;
    if (!container) return;
    const { scrollTop } = container; // 获取滚动偏移量

    // 1. 计算起始渲染索引
    const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
    // 2. 计算结束渲染索引
    const endIndex = startIndex + VISIBLE_COUNT;
    // 3. 截取需要渲染的数据(核心:只渲染视口内的部分数据)
    const showList = dataList.slice(startIndex, endIndex);
    // 4. 计算空白占位高度(模拟视口上方的未渲染内容)
    const top = startIndex * ITEM_HEIGHT;

    // 更新状态:渲染数据 + 空白占位高度
    setRenderList(showList);
    setPaddingTop(top);
  };

  // 初始化渲染:页面加载时,渲染第一屏数据
  useEffect(() => {
    const container = listContainerRef.current;
    if (!container) return;
    handleScroll(); // 首次执行滚动逻辑
    // 绑定滚动事件
    container.addEventListener('scroll', handleScroll);
    // 组件卸载:解绑滚动事件,避免内存泄漏
    return () => container.removeEventListener('scroll', handleScroll);
  }, [dataList]); // 依赖:列表数据源变化时重新初始化

  return (
    <div 
      ref={listContainerRef}
      style={{ 
        height: `${VIEWPORT_HEIGHT}px`, 
        overflow: 'auto',
        border: '1px solid #ccc',
        width: '500px',
        margin: '20px auto'
      }}
    >
      {/* 核心:滚动容器内部的列表区域 */}
      <div 
        style={{ 
          paddingTop: `${paddingTop}px`, // 空白占位区,模拟上方未渲染内容
          transition: 'padding-top 0ms' // 取消过渡,避免滚动时paddingTop变化的动画卡顿
        }}
      >
        {/* 只渲染截取后的少量数据 */}
        {renderList.map((item, index) => (
          <ListItem 
            key={item.id} 
            index={Math.floor(paddingTop / ITEM_HEIGHT) + index} 
            data={item.content} 
          />
        ))}
      </div>
    </div>
  );
};

// 页面入口组件
const App = () => {
  // 生成10000条测试数据,模拟超长列表
  const generateData = () => {
    const list = [];
    for (let i = 0; i < 10000; i++) {
      list.push({ id: i, content: `我是测试数据-${Math.random().toFixed(4)}` });
    }
    return list;
  };
  const dataList = generateData();

  return (
    <div>
      <h2 style={{ textAlign: 'center' }}>React 手写虚拟列表(10000条数据)</h2>
      <VirtualList dataList={dataList} />
    </div>
  );
};

export default App;

运行效果 & 验证

  1. 页面加载后,查看浏览器「Elements」面板,会发现 list-item 节点只有 12个(固定);
  2. 滚动列表时,DOM节点数量始终不变,滚动丝滑无任何卡顿;
  3. 滚动条的长度和原生长列表一致,滚动行为完全无感知差异。

六、进阶优化:虚拟列表的常见增强方案

上面的基础版是定高、基础功能,满足绝大多数业务场景,但是实际开发中会遇到一些特殊需求,这里给出3个高频的进阶优化方案,都是生产级可用的:

优化1:处理「不定高列表项」场景(最常用进阶需求)

基础版依赖「固定项高」,但很多场景下列表项高度不固定(比如图文混排、内容长度不一致),解决方案有2种,按优先级推荐:

方案A:预估高度 + 滚动修正(推荐,性价比最高)

  1. 给每一项设置一个「预估高度」(比如 60px),按基础版逻辑渲染;
  2. 列表项渲染完成后,用 ref 获取真实DOM高度,并更新到数据中;
  3. 滚动时,用「真实高度」重新计算起始索引和占位高度,修正偏移误差。

方案B:分区渲染(适合高度差异极大的场景)

把长列表分成多个「区块」,每个区块内部是定高的列表项,区块本身是不定高的,只渲染视口内的区块,本质是「大虚拟 + 小原生」。

优化2:节流处理滚动事件(性能再提升)

基础版中,onScroll 事件是实时触发的(滚动时每秒触发60+次),虽然计算逻辑简单,但在低端设备上可能有性能损耗。 解决方案:给滚动事件加节流(throttle),限制触发频率(比如每100ms触发一次)。

jsx
// 简单节流函数
const throttle = (fn, delay = 100) => {
  let timer = null;
  return (...args) => {
    if (!timer) {
      timer = setTimeout(() => {
        fn(...args);
        timer = null;
      }, delay);
    }
  };
};
// 滚动事件用节流包裹
const handleScroll = throttle(() => {
  // 原有计算逻辑不变
});

注意:不要用防抖(debounce),防抖会导致滚动停止后才更新列表,出现白屏!

优化3:数据分片加载(海量数据场景,比如10万+条)

如果列表数据量是10万级甚至百万级,即使做了虚拟列表,一次性生成10万条数据的数组也会占用大量内存,导致页面初始化卡顿。 解决方案:分片加载 + 虚拟列表结合

  1. 只在内存中存储「当前视口+前后缓冲区」的少量数据(比如200条);
  2. 滚动到列表边缘时(比如滚动到80%),异步加载下一批数据,同时销毁上一批无用数据;
  3. 结合后端分页接口,实现「无限滚动 + 虚拟列表」,内存占用始终保持极低。

七、成熟的 React 虚拟列表第三方库推荐(生产首选)

手写的虚拟列表适合理解原理、简单场景,但在生产环境中,面对复杂需求(不定高、横向滚动、网格布局、拖拽排序等),推荐直接使用成熟的第三方库,这些库已经封装了所有优化方案,稳定性和性能拉满,99%的业务场景直接用库即可,不用重复造轮子。

首选推荐

1. react-window

  • 官网:https://react-window.vercel.app/
  • 核心优势:轻量(只有3KB gzip)、高性能、API简洁、无冗余依赖,由 React 核心团队成员开发,是 React 生态中虚拟列表的「事实标准」。
  • 支持:定高/不定高列表、横向滚动、网格布局(虚拟网格)、无限滚动,完美兼容所有 React 版本。
  • 用法极简:封装了 FixedSizeList(定高列表)、VariableSizeList(不定高列表)等组件,一行代码替换原生列表。
jsx
import { FixedSizeList } from 'react-window';

const MyVirtualList = ({ data }) => {
  const Row = ({ index, style }) => (
    <div style={style}>第{index}条数据:{data[index].content}</div>
  );

  return (
    <FixedSizeList
      height={500} // 容器高度
      width="100%" // 容器宽度
      itemCount={data.length} // 总数据量
      itemSize={50} // 单个列表项高度
    >
      {Row}
    </FixedSizeList>
  );
};

2. react-virtualized

  • 官网:https://github.com/bvaughn/react-virtualized
  • 核心优势:功能超级全,是 react-window 的「超集」,支持虚拟列表、虚拟表格、虚拟网格、虚拟树、日历等所有长列表相关场景。
  • 缺点:体积稍大(17KB gzip),API 相对复杂一点。
  • 适用场景:复杂业务需求(比如带筛选、排序、分页的虚拟表格)。

两个库的关系

react-window 和 react-virtualized 是同一个作者开发的,react-window 是 react-virtualized 的「轻量化重构版」,作者认为绝大多数场景不需要那么多复杂功能,所以做了精简,性能更好,体积更小。

选型建议:

  • 90%的场景 → 用 react-window;
  • 10%的复杂场景(虚拟表格、树形列表)→ 用 react-virtualized。

八、总结

  1. 虚拟列表的本质:只渲染视口内的列表项,用「空白占位区」模拟视口外的内容,DOM节点数量恒定;
  2. 核心解决的问题:原生长列表的「DOM节点过多」导致的渲染慢、滚动卡顿、内存占用高;
  3. 定高方案核心四步:计算可视数量 → 监听滚动获取scrollTop → 计算起始/结束索引 → 设置空白占位高度;
  4. 灵魂技巧:paddingTop 实现的空白占位区,是虚拟列表能正常滚动的核心;
  5. 进阶优化:不定高用「预估高度+真实高度修正」、滚动事件加节流、海量数据分片加载;
  6. 生产选型:简单场景手写,复杂场景直接用 react-window(首选)或 react-virtualized。
评论
0/100