react-list vs react-infinite vs react-virtual vs react-virtualized vs react-window vs react-window-infinite-loader
React 列表虚拟化与无限滚动方案选型
react-listreact-infinitereact-virtualreact-virtualizedreact-windowreact-window-infinite-loader类似的npm包:

React 列表虚拟化与无限滚动方案选型

react-virtualized、react-window、react-virtual 等库旨在解决渲染长列表时的性能瓶颈。它们通过“虚拟化”技术,只渲染用户可见区域内的 DOM 节点,从而大幅降低内存占用和重绘成本。react-infinite 和 react-list 是较早的解决方案,而 react-window 和 react-virtual 代表了更现代、更轻量或更灵活的架构方向。react-window-infinite-loader 则是配合 react-window 处理异步数据加载的专用工具。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
react-list537,4111,97135.3 kB732 个月前MIT
react-infinite02,683243 kB102-BSD-3-Clause
react-virtual07,085158 kB112-MIT
react-virtualized027,0812.24 MB12 年前MIT
react-window017,206216 kB11 个月前MIT
react-window-infinite-loader095123 kB07 个月前MIT

React 列表虚拟化方案深度对比:架构、性能与实战选型

在构建数据密集型的前端应用时,渲染成千上万个列表项往往会导致页面卡顿甚至崩溃。为了解决这个问题,我们使用“虚拟化”技术 —— 只渲染用户当前能看到的元素。本文将对主流的 6 个 React 虚拟化库进行深度技术剖析,帮助你做出准确的架构决策。

🏗️ 核心架构:有头组件 vs 无头逻辑

不同库对“控制权”的分配方式截然不同。有的直接给你封装好的组件,有的只给你计算逻辑。

react-virtualized 提供了一套完整的重型组件系统。

  • 你直接使用它提供的 <List> 或 <Grid> 组件。
  • 内部封装了滚动容器和样式计算,定制外部容器较麻烦。
// react-virtualized: 使用内置组件
import { List } from 'react-virtualized';

<List
  width={600}
  height={400}
  rowCount={1000}
  rowHeight={50}
  rowRenderer={({ index, style }) => (
    <div style={style}>Row {index}</div>
  )}
/>

react-window 提供了轻量级的组件,但结构更清晰。

  • 同样提供 <FixedSizeList> 等组件,但 API 更简洁。
  • 容器样式分离,更容易与现有 CSS 框架集成。
// react-window: 轻量组件
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={400}
  width={600}
  itemCount={1000}
  itemSize={50}
>
  {({ index, style }) => (
    <div style={style}>Row {index}</div>
  )}
</FixedSizeList>

react-virtual (TanStack Virtual) 是完全无头(Headless)的 Hook。

  • 不提供任何 UI 组件,只返回虚拟项的测量数据和样式。
  • 你可以用原生 <div> 或任何自定义组件来渲染列表。
  • 最适合需要完全控制 DOM 结构或跨框架复用的场景。
// react-virtual: 无头 Hook
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList() {
  const parentRef = useRef();
  const virtualizer = useVirtualizer({
    count: 1000,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(item => (
          <div
            key={item.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${item.start}px)`,
            }}
          >
            Row {item.index}
          </div>
        ))}
      </div>
    </div>
  );
}

react-infinite 封装了滚动检测和加载逻辑。

  • 专注于无限滚动场景,内部处理了容器事件。
  • 灵活性较低,难以适配复杂的自定义布局。
// react-infinite: 专注无限滚动
import Infinite from 'react-infinite';

<Infinite
  elementHeight={50}
  containerHeight={400}
  onInfiniteLoad={() => loadMoreData()}
>
  {items.map(item => <div key={item.id}>{item.name}</div>)}
</Infinite>

react-list 提供多种渲染模式(统一、简单、统一)。

  • 允许通过 itemRenderer 控制渲染,但配置项较为繁琐。
  • 适合需要特定缓存策略的旧项目。
// react-list: 多模式渲染
import List from 'react-list';

<List
  itemRenderer={(index, key) => <div key={key}>Item {index}</div>}
  length={1000}
  type='uniform'
/>

react-window-infinite-loader 是 react-window 的专用包装器。

  • 它本身不渲染列表,而是包装 react-window 的组件来处理加载状态。
  • 必须与 react-window 配合使用,不能独立存在。
// react-window-infinite-loader: 配合使用
import InfiniteLoader from 'react-window-infinite-loader';
import { FixedSizeList } from 'react-window';

<InfiniteLoader
  isItemLoaded={index => index < items.length}
  loadMoreItems={() => fetchNextPage()}
  itemCount={items.length + 1}
>
  {({ onItemsRendered, ref }) => (
    <FixedSizeList
      ref={ref}
      onItemsRendered={onItemsRendered}
      itemCount={1000}
      itemSize={50}
    >
      {({ index, style }) => <div style={style}>{items[index]}</div>}
    </FixedSizeList>
  )}
</InfiniteLoader>

📉 性能与维护状态:遗留系统 vs 现代标准

选择库不仅仅是看功能,还要看它是否还在被积极维护,以及是否引入了不必要的开销。

react-virtualized 是这一领域的先驱,但已停止主要功能更新。

  • 代码库较重,包含大量不再需要的 polyfill 和旧架构代码。
  • 官方明确建议新用户迁移到 react-window。
  • ⚠️ 警告:新项目不应使用此库,除非是维护无法重构的旧代码。
// react-virtualized: 遗留 API 风格
// 需要引入沉重的包路径
import { List } from 'react-virtualized/dist/es/List';

react-window 是为现代浏览器重新设计的轻量级替代品。

  • 去除了 react-virtualized 中的冗余代码,包体积更小。
  • 性能更优,特别是在快速滚动时的帧率表现。
  • 由同一作者维护,是目前的行业标准。
// react-window: 现代 ES 模块
import { FixedSizeList } from 'react-window';

react-virtual 基于最新的 React Hooks 构建。

  • 没有类组件包袱,逻辑与视图分离彻底。
  • 更新频率高,紧跟 React 生态(如支持 React 18 并发特性)。
  • 适合追求极致性能和自定义能力的团队。
// react-virtual: Hooks 优先
import { useVirtualizer } from '@tanstack/react-virtual';

react-infinite 和 react-list 属于早期生态产物。

  • 更新频率低,社区支持逐渐减少。
  • 在处理复杂动态高度或网格布局时,性能不如现代方案。
  • ⚠️ 建议:仅用于维护旧项目,新项目请避开。
// react-infinite: 较旧的回调模式
elementHeight={50} // 硬编码高度,动态支持较弱

react-window-infinite-loader 保持了与 react-window 一致的维护节奏。

  • 专门解决虚拟化列表中的异步数据竞争问题。
  • 避免了手动计算滚动位置和加载状态容易出现的 Bug。
// react-window-infinite-loader: 标准化加载流
isItemLoaded={index => !!items[index]} // 清晰的加载判断

🔄 动态高度与复杂布局支持

列表项高度不固定是虚拟化中最棘手的问题之一,不同库的处理方式差异很大。

react-virtualized 支持动态高度但配置复杂。

  • 需要使用 <CellMeasurer> 包裹每个元素进行测量。
  • 首次渲染会有闪烁,因为需要先测量再定位。
// react-virtualized: 测量器模式
<CellMeasurerCache defaultHeight={100}>
  <CellMeasurer>
    {({ measure, height }) => (
      <div style={{ height }} onLoad={measure}>Content</div>
    )}
  </CellMeasurer>
</CellMeasurerCache>

react-window 简化了动态高度处理。

  • 提供 <VariableSizeList> 组件。
  • 需要手动调用 resetAfterIndex 通知高度变化,逻辑更可控。
// react-window: 可变尺寸列表
<VariableSizeList
  itemSize={index => getHeightForItem(index)}
  itemCount={1000}
>
  {({ index, style }) => <div style={style}>Item {index}</div>}
</VariableSizeList>

react-virtual 提供自动测量 Hook。

  • useVirtualizer 支持 estimateSize 和自动观察器。
  • 可以配合 ResizeObserver 实现更平滑的动态高度调整。
// react-virtual: 动态测量支持
const virtualizer = useVirtualizer({
  count: items.length,
  estimateSize: () => 100,
  measureElement: element => element.getBoundingClientRect().height,
});

react-infinite 对动态高度支持有限。

  • 通常要求所有元素高度一致,或者通过复杂配置实现。
  • 在高度变化频繁时容易出现滚动跳动。
// react-infinite: 高度限制
elementHeight={this.props.containerHeight} // 通常期望固定值

react-list 提供多种策略处理高度。

  • 通过 type="simple" 或 "uniform" 区分,但 API 不够直观。
  • 性能在大量动态变化时不如 react-window 稳定。
// react-list: 类型配置
type='simple' // 尝试处理动态高度,但性能有损耗

react-window-infinite-loader 继承 react-window 的能力。

  • 本身不处理高度,完全依赖底层列表组件(如 VariableSizeList)。
  • 确保加载新数据后滚动位置计算正确。
// react-window-infinite-loader: 透传属性
<InfiniteLoader>
  {({ ref }) => <VariableSizeList ref={ref} ... />}
</InfiniteLoader>

🛠️ 实际场景选型指南

场景 1:标准后台管理表格

  • 需求:数据量大,行高固定,需要快速开发。
  • 推荐:react-window
  • 理由:API 简单,性能稳定,社区示例多,足够满足 90% 的表格需求。

场景 2:高度定制化的社交信息流

  • 需求:每条内容高度不同,包含图片、视频,需要自定义滚动条和动画。
  • 推荐:react-virtual
  • 理由:无头设计让你完全控制 DOM 结构,方便集成自定义滚动容器和复杂的布局逻辑。

场景 3:无限滚动的商品列表

  • 需求:滚动到底部自动加载下一页,保持滚动位置不跳动。
  • 推荐:react-window + react-window-infinite-loader
  • 理由:这是官方推荐的最佳实践组合,专门解决了异步加载时的滚动计算难题。

场景 4:维护 5 年前的老项目

  • 需求:代码库中已经大量使用了某个库,且预算不足以重构。
  • 推荐:保持现状 或 react-virtualized
  • 理由:如果已经是 react-virtualized,暂时不要动。如果是 react-infinite,建议制定计划迁移到 react-window。

📊 核心特性对比总结

特性react-windowreact-virtualreact-virtualizedreact-infinite
架构模式轻量组件无头 Hook重型组件封装组件
维护状态✅ 活跃✅ 非常活跃⚠️ 维护模式⚠️ 低频更新
动态高度支持 (需手动通知)支持 (自动观察)支持 (测量器)有限支持
包体积小小大中
学习曲线低中中低
无限加载需配合 Loader需自行实现内置部分支持内置支持

💡 架构师建议

在现代前端架构中,react-window 是大多数情况下的默认选择。它平衡了易用性、性能和生态支持。如果你的团队已经在使用 TanStack 生态(如 TanStack Query/Table),那么 react-virtual 是更自然的选择,因为它的 Hooks 风格与现有代码库更融合。

对于 react-virtualized、react-infinite 和 react-list,请将它们视为“遗留技术”。除非你有不可抗力的历史包袱,否则不要在新代码库中引入它们。技术债的利息往往比初期节省的开发时间要高得多。

最后,记住虚拟化不是银弹。如果列表只有 50 项,直接用原生 <div> 渲染通常比引入虚拟化库更快、更简单。只在数据量真正影响性能时,再引入这些工具。

如何选择: react-list vs react-infinite vs react-virtual vs react-virtualized vs react-window vs react-window-infinite-loader

  • react-list:

    适用于需要支持可变高度列表且对虚拟化要求不极端的场景。它提供了一些布局灵活性,但社区活跃度较低,若遇到复杂交互问题可能难以获得支持。

  • react-infinite:

    如果你的项目非常古老且无法升级依赖,或者只需要最简单的无限滚动且数据量不大,可以考虑它。但在现代架构中,它的性能优化和 API 设计已显落后,不建议在新项目中使用。

  • react-virtual:

    选择 react-virtual (TanStack Virtual) 如果你需要无头(headless)逻辑,希望完全控制 DOM 渲染,或者需要在 React 之外的框架(如 Vue、Svelte)中复用虚拟化逻辑。它最适合高度定制化的 UI 需求。

  • react-virtualized:

    这是一个历史悠久的库,但已处于维护模式,官方推荐迁移至 react-window。除非你在维护遗留系统且无法承担重构成本,否则不应在新项目中选用。

  • react-window:

    这是目前最推荐的通用方案。它轻量、稳定、由 react-virtualized 原作者维护。如果你需要标准的固定高度或动态高度列表,且希望开箱即用,这是首选。

  • react-window-infinite-loader:

    当你已经选定 react-window 且需要实现“滚动到底部自动加载下一页”的功能时,必须搭配此库。它专门处理加载状态、数据拼接和滚动位置保持,避免重复造轮子。

react-list的README

ReactList

A versatile infinite scroll React component.

Install

bower install react-list

# or

npm install react-list

ReactList depends on React.

Examples

Check out the example page and the the example page source for examples of different configurations.

Here's another simple example to get you started.

import loadAccount from 'my-account-loader';
import React from 'react';
import ReactList from 'react-list';

class MyComponent extends React.Component {
  state = {
    accounts: []
  };

  componentWillMount() {
    loadAccounts(::this.handleAccounts);
  }

  handleAccounts(accounts) {
    this.setState({accounts});
  }

  renderItem(index, key) {
    return <div key={key}>{this.state.accounts[index].name}</div>;
  }

  render() {
    return (
      <div>
        <h1>Accounts</h1>
        <div style={{overflow: 'auto', maxHeight: 400}}>
          <ReactList
            itemRenderer={::this.renderItem}
            length={this.state.accounts.length}
            type='uniform'
          />
        </div>
      </div>
    );
  }
}

Props

axis (defaults to y)

The axis that this list scrolls on.

initialIndex

An index to scroll to after mounting.

itemRenderer(index, key)

A function that receives an index and a key and returns the content to be rendered for the item at that index.

itemsRenderer(items, ref)

A function that receives the rendered items and a ref. By default this element is just a <div>. Generally it only needs to be overridden for use in a <table> or other special case. NOTE: You must set ref={ref} on the component that contains the items so the correct item sizing calculations can be made.

itemSizeEstimator(index, cache)

A function that receives an item index and the cached known item sizes and returns an estimated size (height for y-axis lists and width for x-axis lists) of that item at that index. This prop is only used when the prop type is set to variable and itemSizeGetter is not defined. Use this property when you can't know the exact size of an item before rendering it, but want it to take up space in the list regardless.

itemSizeGetter(index)

A function that receives an item index and returns the size (height for y-axis lists and width for x-axis lists) of that item at that index. This prop is only used when the prop type is set to variable.

length (defaults to 0)

The number of items in the list.

minSize (defaults to 1)

The minimum number of items to render at any given time. This can be used to render some amount of items initially when rendering HTML on the server.

pageSize (defaults to 10)

The number of items to batch up for new renders. Does not apply to 'uniform' lists as the optimal number of items is calculated automatically.

scrollParentGetter (defaults to finding the nearest scrollable parent)

A function that returns a DOM Element or Window that will be treated as the scrolling container for the list. In most cases this does not need to be set for the list to work as intended. It is exposed as a prop for more complicated uses where the scrolling container may not initially have an overflow property that enables scrolling.

scrollParentViewportSizeGetter (defaults to scrollParent's viewport size)

A function that returns the size of the scrollParent's viewport. Provide this prop if you can efficiently determine your scrollParent's viewport size as it can improve performance.

threshold (defaults to 100)

The number of pixels to buffer at the beginning and end of the rendered list items.

type (one of simple, variable, or uniform, defaults to simple)
  • simple This type is...simple. It will not cache item sizes or remove items that are above the viewport. This type is sufficient for many cases when the only requirement is incremental rendering when scrolling.

  • variable This type is preferred when the sizes of the items in the list vary. Supply the itemSizeGetter when possible so the entire length of the list can be established beforehand. Otherwise, the item sizes will be cached as they are rendered so that items that are above the viewport can be removed as the list is scrolled.

  • uniform This type is preferred when you can guarantee all of your elements will be the same size. The advantage here is that the size of the entire list can be calculated ahead of time and only enough items to fill the viewport ever need to be drawn. The size of the first item will be used to infer the size of every other item. Multiple items per row are also supported with this type.

useStaticSize (defaults to false)

Set to true if the item size will never change (as a result of responsive layout changing or otherwise). This prop is only used when the prop type is set to uniform. This is an opt-in optimization that will cause the very first element's size to be used for all elements for the duration of the component's life.

useTranslate3d (defaults to false)

A boolean to determine whether the translate3d CSS property should be used for positioning instead of the default translate. This can help performance on mobile devices, but is supported by fewer browsers.

getListStyle({ x, y })

A function to override the default translate: transform positioning behavior. For example, you could set getListStyle={({ x, y }) => ({ position: 'relative', left: x, top: y })} to use relative positioning.

Methods

scrollTo(index)

Put the element at index at the top of the viewport. Note that if you aren't using type='uniform' or an itemSizeGetter, you will only be able to scroll to an element that has already been rendered.

scrollAround(index)

Scroll the viewport so that the element at index is visible, but not necessarily at the top. The scrollTo note above also applies to this method.

getVisibleRange() => [firstIndex, lastIndex]

Return the indices of the first and last items that are at all visible in the viewport.

FAQ

What is "ReactList failed to reach a stable state."?

This happens when specifying the uniform type without actually providing uniform size elements. The component attempts to draw only the minimum necessary elements at one time and that minimum element calculation is based off the first element in the list. When the first element does not match the other elements, the calculation will be wrong and the component will never be able to fully resolve the ideal necessary elements.

Why doesn't it work with margins?

The calculations to figure out element positioning and size get significantly more complicated with margins, so they are not supported. Use a transparent border or padding or an element with nested elements to achieve the desired spacing.

Why is there no onScroll event handler?

If you need an onScroll handler, just add the handler to the div wrapping your ReactList component:

<div style={{height: 300, overflow: 'auto'}} onScroll={this.handleScroll}>
  <ReactList ... />
</div>

Development

open docs/index.html
make