关于 virtual-lists
面向 React 开发者的列表虚拟化指南,介绍仅渲染可视区域内容以改善大型列表和网格的渲染性能,并说明适用场景与限制。
主要功能
- 01仅渲染可视区域的行或单元格
- 02支持固定尺寸和可变尺寸列表
- 03支持水平与垂直虚拟化网格
- 04结合滚动按需加载数据
- 05说明无障碍与动态高度限制
适用场景
- 01优化含数百或数千项的列表
- 02降低长列表的初始渲染时间
- 03改善大型表格的滚动表现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill virtual-lists安装后在 Claude Code 中输入/virtual-lists确认已加载。
详细介绍
列表虚拟化
目录
列表虚拟化(也称为窗口化)是指在动态列表中只渲染可见的内容行,而不是整个列表。渲染的行只是完整列表中的一个小子集,用户滚动时,可见内容(窗口)也会随之移动。这可以提高渲染性能。
如果你使用 React,并且需要高效显示大型数据列表,你可能熟悉 react-virtualized。这是由 Brian Vaughn 开发的窗口化库,只渲染列表中当前可见的项目(位于可滚动的“视口”内)。这意味着你不必承担一次性渲染数千行数据的开销。
适用场景
- 渲染大型列表或网格(数百/数千个项目)时,如果它们导致性能问题,可使用此方案
- 这有助于缩短初始渲染时间并提升滚动性能
不适用场景
- 对于较短的列表(少于约 100 个项目),原生渲染已足够快,无需使用虚拟化
- 当无障碍要求规定屏幕阅读器需要在 DOM 中访问所有列表项时
- 当列表项的高度不可预测且取决于内容,导致虚拟化测量不可靠时
说明
- 使用
react-window(或react-virtualized),在可滚动容器中只渲染可见项目 - 对于高度相同的项目,选择
FixedSizeList;对于高度不同的项目,选择VariableSizeList - 使用
react-window-infinite-loader,在用户滚动时逐步加载数据 - 对于不需要完整虚拟化的简单场景,可考虑使用 CSS
content-visibility: auto
详情
列表虚拟化如何工作?
对列表项进行“虚拟化”,需要维护一个窗口,并让这个窗口在列表中移动。react-virtualized 中的窗口化方式如下:
- 使用一个带相对定位的小型容器 DOM 元素(例如
<ul>)作为窗口 - 使用一个用于滚动的大型 DOM 元素
- 在容器内绝对定位子元素,并为其设置 top、left、width 和 height 样式。
虚拟化并非一次性渲染列表中的数千个元素(这可能导致初始渲染变慢或影响滚动性能),而是专注于只渲染用户可见的项目。
这有助于在中低端设备上保持列表渲染速度。用户滚动时,你可以获取/显示更多项目,同时卸载之前的条目并用新条目替换。
react-virtualized 的轻量替代方案
react-window 是由同一作者重写的 react-virtualized,旨在让它体积更小、速度更快且更易于 tree-shake。
对于支持 tree-shake 的库,其体积取决于你选择使用哪些 API。与使用 react-virtualized 相比,使用它可节省约 20~30 KB(gzip 压缩后)。
这两个包的 API 相似;在两者存在差异的地方,react-window 往往更简单。react-window 的组件包括:
列表
列表会渲染窗口化列表(行),也就是说,只向用户显示可见行(例如 FixedSizeList、VariableSizeList)。列表内部使用 Grid 来渲染行,并将属性传递给内部的 Grid。
使用 React 渲染数据列表
以下是使用 React 渲染简单数据(itemsArray)列表示例:
import React from "react";
import ReactDOM from "react-dom";
const itemsArray = [
{ name: "Drake" },
{ name: "Halsey" },
{ name: "Camillo Cabello" },
{ name: "Travis Scott" },
{ name: "Bazzi" },
{ name: "Flume" },
{ name: "Nicki Minaj" },
{ name: "Kodak Black" },
{ name: "Tyga" },
{ name: "Buno Mars" },
{ name: "Lil Wayne" }, ...
]; // our data
const Row = ({ index, style }) => (
<div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
{itemsArray[index].name}
</div>
);
const Example = () => (
<div
style={{
height: 150,
width: 300
}}
class="List"
>
{itemsArray.map((item, index) => Row({ index }))}
</div>
);
ReactDOM.render(<Example />, document.getElementById("root"));
使用 react-window 渲染列表
……下面是使用 react-window 的 FixedSizeList 实现的相同示例。它接收几个属性(width、height、itemCount、itemSize),以及作为子元素传入的行渲染函数:
import React from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from "react-window";
const itemsArray = [...]; // our data
const Row = ({ index, style }) => (
<div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
{itemsArray[index].name}
</div>
);
const Example = () => (
<List
className="List"
height={150}
itemCount={itemsArray.length}
itemSize={35}
width={300}
>
{Row}
</List>
);
ReactDOM.render(<Example />, document.getElementById("root"));
你可以在 CodeSandbox 上试用 FixedSizeList。
Grid
Grid 会对表格数据进行垂直和水平方向的虚拟化渲染(例如 FixedSizeGrid、VariableSizeGrid)。它只会根据当前的水平和垂直滚动位置,渲染填满 Grid 所需的单元格。
import React from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeGrid as Grid } from 'react-window';
const itemsArray = [
[{},{},{},...],
[{},{},{},...],
[{},{},{},...],
[{},{},{},...],
];
const Cell = ({ columnIndex, rowIndex, style }) => (
<div
className={
columnIndex % 2
? rowIndex % 2 === 0
? 'GridItemOdd'
: 'GridItemEven'
: rowIndex % 2
? 'GridItemOdd'
: 'GridItemEven'
}
style={style}
>
{itemsArray[rowIndex][columnIndex].name}
</div>
);
const Example = () => (
<Grid
className="Grid"
columnCount={5}
columnWidth={100}
height={150}
rowCount={5}
rowHeight={35}
width={300}
>
{Cell}
</Grid>
);
ReactDOM.render(<Example />, document.getElementById('root'));
你也可以在 CodeSandbox 上试用 FixedSizeGrid。
更深入的 react-window 示例
Scott Taylor 使用 react-window 和 FixedSizeGrid 实现了一个开源的 Pitchfork 音乐评论爬虫 (src)。
Pitchfork 爬虫使用了 react-window-infinite-loader(演示),它能将大型数据集拆分为多个区块,以便在滚动到可视范围时加载。
以下是该应用中集成 react-window-infinite-loader 的代码片段:
import React, { Component } from 'react';
import { FixedSizeGrid as Grid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
// ...
render() {
return (
<InfiniteLoader
isItemLoaded={this.isItemLoaded}
loadMoreItems={this.loadMoreItems}
itemCount={this.state.count + 1}
>
{({ onItemsRendered, ref }) => (
<Grid
onItemsRendered={this.onItemsRendered(onItemsRendered)}
columnCount={COLUMN_SIZE}
columnWidth={180}
height={800}
rowCount={Math.max(this.state.count / COLUMN_SIZE)}
rowHeight={220}
width={1024}
ref={ref}
>
{this.renderCell}
</Grid>
)}
</InfiniteLoader>
);
}
将该应用从 react-virtualized 迁移过来的提交或许对你有帮助。
此外,还有一个使用 FixedSizeList 的实现:
return (
<InfiniteLoader
isItemLoaded={this.isItemLoaded}
loadMoreItems={this.loadMoreItems}
itemCount={this.state.count}
>
{({ onItemsRendered, ref }) => (
<section>
<FixedSizeList
itemCount={this.state.count}
itemSize={ROW_HEIGHT}
onItemsRendered={onItemsRendered}
height={this.state.height}
width={this.state.width}
ref={ref}
>
{this.renderCell}
</FixedSizeList>
</section>
)}
</InfiniteLoader>
);
对于更复杂的需求,The Movie Database 的一个演示应用在底层使用了 react-virtualized 和 Infinite Loader。将它迁移到 react-window 和 react-window-infinite-loader 并没有花太多时间,不过我们发现有几个组件尚未得到支持。尽管如此,最终功能已经相当接近。
缺少的组件是 WindowScroller 和 AutoSizer,我们接下来会介绍它们。
// ...
return (
<section>
<AutoSizer disableHeight>
{({width}) => {
const {movies, hasMore} = this.props;
const rowCount = getRowsAmount(width, movies.length, hasMore);
// ...
return (
<InfiniteLoader
ref={this.infiniteLoaderRef}
// ...
{({onRowsRendered, registerChild}) => (
<WindowScroller>
{({height, scrollTop}) => (
react-window 缺少什么?
react-window 尚未具备 react-virtualized 的完整 API,因此如果你正在考虑使用它,请查看比较文档。它缺少什么?
- WindowScroller - 这是
react-virtualized的一个组件,可根据窗口的滚动位置滚动列表。目前没有计划为 react-window 实现此功能,因此你需要自行解决。 - AutoSizer - 一个 HOC,可扩展以填充所有可用空间,并自动调整单个子元素的宽度和高度。Brian 将其实现为一个独立包。请关注此问题,了解最新进展。
- CellMeasurer - 一个 HOC,通过以用户不可见的方式渲染单元格内容来自动测量其尺寸。请在此处关注关于支持情况的讨论。
尽管如此,我们发现,凭借开箱即用的功能,react-window 已足以满足我们的大多数需求。
Web 平台的改进
一些现代浏览器现在支持 CSS content-visibility。content-visibility:auto 可以跳过屏幕外内容的渲染和绘制,直到需要时再处理。如果你有一份渲染成本高昂的长篇 HTML 文档,不妨试试这个属性。
对于渲染动态内容列表,我仍建议使用 react-window 这样的库。很难实现一个基于 content-visibility:hidden 的此类库,并让它胜过积极使用 display:none 或在内容位于屏幕外时移除 DOM 节点的版本;如今许多列表虚拟化库可能都会这样做。