mcpField
V
virtual-lists

virtual-lists

github.com/patternsdev开发2571.5k 次安装28 周前更新
代码仓库

关于 virtual-lists

面向 React 开发者的列表虚拟化指南,介绍仅渲染可视区域内容以改善大型列表和网格的渲染性能,并说明适用场景与限制。

主要功能

  1. 01仅渲染可视区域的行或单元格
  2. 02支持固定尺寸和可变尺寸列表
  3. 03支持水平与垂直虚拟化网格
  4. 04结合滚动按需加载数据
  5. 05说明无障碍与动态高度限制

适用场景

  1. 01优化含数百或数千项的列表
  2. 02降低长列表的初始渲染时间
  3. 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 节点的版本;如今许多列表虚拟化库可能都会这样做。

源码

参考资料

评论

支持 Markdown