使用 React-window 虚拟化大型列表

超大型的表格和列表可能会显著降低网站的性能。虚拟化可以助您一臂之力!

react-window 是一个库,可高效地呈现大型列表。

以下示例展示了一个使用 react-window 呈现的 1,000 行列表。尝试尽可能快速地滚动。

为什么搜索渠道报告非常实用?

有时,您可能需要显示包含多行的大型表或列表。加载此类列表中的每一项都会显著影响性能。

列表虚拟化(或称“窗口化”)是指仅渲染用户可见的内容的概念。最开始渲染的元素数量仅占整个列表的一小部分,当用户继续滚动时,可见内容的“窗口”会移动。这可以改善列表的渲染和滚动性能。

虚拟化列表中的内容窗口
在虚拟化列表中移动内容的“窗口”

系统会回收退出“窗口”的 DOM 节点,或在用户向下滚动列表时立即将其替换为新的元素。这样可以保留特定于窗口大小的所有渲染元素的数量。

回应窗口

react-window 是一个小型第三方库,可让您更轻松地在应用中创建虚拟化列表。它提供了许多可用于不同类型的列表和表的基本 API。

何时使用固定大小的列表

如果您有一个包含大小相同的项的一维长列表,请使用 FixedSizeList 组件。

import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const ListComponent = () => (
  <FixedSizeList
    height={500}
    width={500}
    itemSize={120}
    itemCount={items.length}
  >
    {Row}
  </FixedSizeList>
);

export default ListComponent;
  • FixedSizeList 组件接受 heightwidthitemSize 属性,用于控制列表中项的大小。
  • 呈现这些行的函数会作为子项传递给 FixedSizeList。您可以使用 index 参数 (items[index]) 访问特定项的详细信息。
  • 此外,系统还会向行呈现方法传入 style 参数,该方法必须附加到行元素。列表项是绝对定位且以内嵌方式分配其高度和宽度值,而这一点是由 style 参数导致的。

本文前面显示的 Glitch 示例展示了一个 FixedSizeList 组件的示例。

何时使用大小可变的列表

使用 VariableSizeList 组件可呈现具有不同大小的项列表。此组件的工作方式与固定大小的列表相同,但要求 itemSize 属性的函数而不是特定值。

import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const getItemSize = index => {
  // return a size for items[index]
}

const ListComponent = () => (
  <VariableSizeList
    height={500}
    width={500}
    itemCount={items.length}
    itemSize={getItemSize}
  >
    {Row}
  </VariableSizeList>
);

export default ListComponent;

以下嵌入展示了此组件的示例。

在此示例中,传递给 itemSize 属性的项大小函数会对行高进行随机化处理。然而,在真实应用中,应该有实际的逻辑来定义每个项的尺寸。理想情况下,这些大小应根据数据计算或从 API 获取。

网格

react-window 还支持虚拟化多维列表或网格。在此情况下,可见内容的“窗口”会随着用户水平滚动和垂直滚动而变化。

在虚拟化网格中移动内容窗口是二维的
在虚拟化网格中移动内容“窗口”是二维的

同样,可以根据特定列表项的大小是否不同而使用 FixedSizeGridVariableSizeGrid 组件。

  • 对于 FixedSizeGrid,API 大致相同,但需要针对列和行都表示高度、宽度和项数。
  • 对于 VariableSizeGrid,可以通过将函数(而不是值)传递给其各自的属性来更改列宽和行高。

请参阅此文档,查看虚拟化网格的示例。

滚动时延迟加载

许多网站会等到用户向下滚动后再加载并呈现长列表中的项,从而提高性能。这种方法通常称为“无限加载”,当用户滚动经过靠近末尾的特定阈值时,系统会将新的 DOM 节点添加到列表中。尽管这比一次性加载列表上的所有项要好,但如果用户滚动浏览过如此多的行条目,最终仍会在 DOM 中填充数千个行条目。这可能会导致 DOM 规模过大,从而导致样式计算和 DOM 变更变慢,进而开始影响性能。

下图可能有助于对此进行总结:

常规列表和虚拟化列表在滚动方面的差异
常规列表和虚拟化列表在滚动方面的差异

解决此问题的最佳方法是继续使用 react-window 等库来维护页面上较小的“窗口”元素,但也要在用户向下滚动时延迟加载较新的条目。单独的软件包 react-window-infinite-loader 通过 react-window 实现了这一点。

请考虑以下代码,它展示了在父 App 组件中管理的状态示例。

import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [], // instantiate initial list here
      moreItemsLoading: false,
      hasNextPage: true
    };

    this.loadMore = this.loadMore.bind(this);
  }

  loadMore() {
   // method to fetch newer entries for the list
  }

  render() {
    const { items, moreItemsLoading, hasNextPage } = this.state;

    return (
      <ListComponent
        items={items}
        moreItemsLoading={moreItemsLoading}
        loadMore={this.loadMore}
        hasNextPage={hasNextPage}
      />
    );
  }
}

export default App;

loadMore 方法会传递到包含无限加载器列表的子 ListComponent 中。这一点很重要,因为当用户滚动经过某个点后,无限加载器需要触发一个回调来加载更多项。

呈现列表的 ListComponent 应如下所示:

import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";

const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
  const Row = ({ index, style }) => (
     {/* define the row component using items[index] */}
  );

  const itemCount = hasNextPage ? items.length + 1 : items.length;

  return (
    <InfiniteLoader
      isItemLoaded={index => index < items.length}
      itemCount={itemCount}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={500}
          width={500}
          itemCount={itemCount}
          itemSize={120}
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {Row}
        </FixedSizeList>
      )}
  </InfiniteLoader>
  )
};

export default ListComponent;

在这里,FixedSizeList 组件封装在 InfiniteLoader 中。分配给加载器的属性如下:

  • isItemLoaded:用于检查特定项是否已加载的方法
  • itemCount:列表中的项数量(或预期数量)
  • loadMoreItems:返回可解析为列表的其他数据的 promise 的回调。

渲染属性用于返回列表组件用于渲染的函数。onItemsRenderedref 属性都是需要传入的属性。

以下示例说明了如何将无限加载与虚拟列表搭配使用。

向下滚动列表可能没有变化,但现在每当您滚动到列表末尾时都会请求从随机用户 API 检索 10 个用户。同时,每次仅渲染一个“窗口”的结果时,即可做到这一点。

通过检查特定项的 index,系统可能会针对项显示不同的加载状态,具体取决于是否已针对较新的条目发出请求且该项仍在加载中。

例如:

const Row = ({ index, style }) => {
  const itemLoading = index === items.length;

  if (itemLoading) {
      // return loading state
  } else {
      // return item
  }
};

过扫描

由于虚拟化列表中的项仅在用户滚动时发生变化,因此当要显示较新的条目时,空白区域可能会短暂闪烁。您可以尝试快速滚动本指南中前面提到的任何示例来注意到这一点。

为了提升虚拟化列表的用户体验,react-window 允许您过度扫描具有 overscanCount 属性的项。这样,您就可以定义始终在可见“窗口”之外呈现多少项内容。

<FixedSizeList
  //...
  overscanCount={4}
>
  {...}
</FixedSizeList>

overscanCount 同时适用于 FixedSizeListVariableSizeList 组件,且默认值为 1。根据列表的大小以及每项内容的大小,过扫描多个条目有助于防止在用户滚动鼠标时明显出现空白区域。但是,过扫描的条目过多可能会对性能产生负面影响。使用虚拟化列表的目的是尽可能减少在任何给定时刻用户可以看到的条目数,因此请尽量减少过扫描项的数量。

对于 FixedSizeGridVariableSizeGrid,请使用 overscanColumnsCountoverscanRowsCount 属性分别控制要过扫描的列数和行数。

总结

如果您不确定从何处开始虚拟化应用中的列表和表,请按以下步骤操作:

  1. 衡量渲染和滚动性能。这篇文章介绍了如何使用 Chrome 开发者工具中的 FPS 计量器来探索列表项的渲染效率。
  2. 对于影响性能的任何长列表或网格,请添加 react-window
  3. 如果 react-window 不支持某些功能,如果您无法自行添加此功能,请考虑使用 react-virtualized
  4. 如果您需要在用户滚动时延迟加载项目,请使用 react-window-infinite-loader 封装虚拟化列表。
  5. 对列表使用 overscanCount 属性,对网格使用 overscanColumnsCountoverscanRowsCount 属性,以防止出现空内容。不要过度扫描太多条目,否则会对性能产生负面影响。