react-virtualized、react-window、react-virtual 等库旨在解决渲染长列表时的性能瓶颈。它们通过“虚拟化”技术,只渲染用户可见区域内的 DOM 节点,从而大幅降低内存占用和重绘成本。react-infinite 和 react-list 是较早的解决方案,而 react-window 和 react-virtual 代表了更现代、更轻量或更灵活的架构方向。react-window-infinite-loader 则是配合 react-window 处理异步数据加载的专用工具。
在构建数据密集型的前端应用时,渲染成千上万个列表项往往会导致页面卡顿甚至崩溃。为了解决这个问题,我们使用“虚拟化”技术 —— 只渲染用户当前能看到的元素。本文将对主流的 6 个 React 虚拟化库进行深度技术剖析,帮助你做出准确的架构决策。
不同库对“控制权”的分配方式截然不同。有的直接给你封装好的组件,有的只给你计算逻辑。
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 更简洁。// 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。
<div> 或任何自定义组件来渲染列表。// 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>
选择库不仅仅是看功能,还要看它是否还在被积极维护,以及是否引入了不必要的开销。
react-virtualized 是这一领域的先驱,但已停止主要功能更新。
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-virtual: Hooks 优先
import { useVirtualizer } from '@tanstack/react-virtual';
react-infinite 和 react-list 属于早期生态产物。
// react-infinite: 较旧的回调模式
elementHeight={50} // 硬编码高度,动态支持较弱
react-window-infinite-loader 保持了与 react-window 一致的维护节奏。
// 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>
react-windowreact-virtualreact-window + react-window-infinite-loaderreact-virtualizedreact-virtualized,暂时不要动。如果是 react-infinite,建议制定计划迁移到 react-window。| 特性 | react-window | react-virtual | react-virtualized | react-infinite |
|---|---|---|---|---|
| 架构模式 | 轻量组件 | 无头 Hook | 重型组件 | 封装组件 |
| 维护状态 | ✅ 活跃 | ✅ 非常活跃 | ⚠️ 维护模式 | ⚠️ 低频更新 |
| 动态高度 | 支持 (需手动通知) | 支持 (自动观察) | 支持 (测量器) | 有限支持 |
| 包体积 | 小 | 小 | 大 | 中 |
| 学习曲线 | 低 | 中 | 中 | 低 |
| 无限加载 | 需配合 Loader | 需自行实现 | 内置部分支持 | 内置支持 |
在现代前端架构中,react-window 是大多数情况下的默认选择。它平衡了易用性、性能和生态支持。如果你的团队已经在使用 TanStack 生态(如 TanStack Query/Table),那么 react-virtual 是更自然的选择,因为它的 Hooks 风格与现有代码库更融合。
对于 react-virtualized、react-infinite 和 react-list,请将它们视为“遗留技术”。除非你有不可抗力的历史包袱,否则不要在新代码库中引入它们。技术债的利息往往比初期节省的开发时间要高得多。
最后,记住虚拟化不是银弹。如果列表只有 50 项,直接用原生 <div> 渲染通常比引入虚拟化库更快、更简单。只在数据量真正影响性能时,再引入这些工具。
适用于需要支持可变高度列表且对虚拟化要求不极端的场景。它提供了一些布局灵活性,但社区活跃度较低,若遇到复杂交互问题可能难以获得支持。
如果你的项目非常古老且无法升级依赖,或者只需要最简单的无限滚动且数据量不大,可以考虑它。但在现代架构中,它的性能优化和 API 设计已显落后,不建议在新项目中使用。
选择 react-virtual (TanStack Virtual) 如果你需要无头(headless)逻辑,希望完全控制 DOM 渲染,或者需要在 React 之外的框架(如 Vue、Svelte)中复用虚拟化逻辑。它最适合高度定制化的 UI 需求。
这是一个历史悠久的库,但已处于维护模式,官方推荐迁移至 react-window。除非你在维护遗留系统且无法承担重构成本,否则不应在新项目中选用。
这是目前最推荐的通用方案。它轻量、稳定、由 react-virtualized 原作者维护。如果你需要标准的固定高度或动态高度列表,且希望开箱即用,这是首选。
当你已经选定 react-window 且需要实现“滚动到底部自动加载下一页”的功能时,必须搭配此库。它专门处理加载状态、数据拼接和滚动位置保持,避免重复造轮子。
A versatile infinite scroll React component.
bower install react-list
# or
npm install react-list
ReactList depends on React.
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>
);
}
}
y)The axis that this list scrolls on.
An index to scroll to after mounting.
A function that receives an index and a key and returns the content to be rendered for the item at that index.
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.
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.
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.
0)The number of items in the list.
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.
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.
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.
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.
100)The number of pixels to buffer at the beginning and end of the rendered list items.
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.
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.
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.
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.
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.
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.
[firstIndex, lastIndex]Return the indices of the first and last items that are at all visible in the viewport.
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.
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.
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>
open docs/index.html
make