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

超大型表格和列表可能会显著降低网站的性能。虚拟化可以帮您解决这个问题!

react-window 是一个可高效渲染大型列表的库。

下面是一个包含 1,000 行的列表通过 react-window 进行渲染的示例。尝试尽可能快速地滚动。

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

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

列表虚拟化(或“窗口化”)是指仅渲染用户可见的内容。最初渲染的元素数量只是整个列表中的一个很小的子集,当用户继续滚动时,可见内容的“窗口”会移动。这样可以提高列表的渲染和滚动性能。

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

退出“窗口”的 DOM 节点会被回收,或者在用户向下滚动列表时立即替换为较新的元素。这样可确保所有渲染元素的数量与窗口大小保持一致。

react-window

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 的回调,该 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 属性,以防止出现内容闪烁的情况。请勿过度扫描过多的条目,否则会影响性能。