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

超大型表格和列表可能会显著降低网站的性能。虚拟化可以帮上忙!

react-window 是一个库,可让您高效地渲染大型列表。

下面是一个使用 react-window 呈现的包含 1,000 行的列表示例。请尝试尽快滚动。

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

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

列表虚拟化(也称为“窗口化”)是一种仅渲染对用户可见的内容的概念。最初呈现的元素数量是整个列表的一小部分,并且当用户继续滚动时,可见内容的“窗口”会移动。这有助于提高列表的渲染和滚动性能。

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

当用户向下滚动列表时,系统会回收退出“窗口”的 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,您可以通过向其各自的 prop 传入函数(而非值)来更改列宽和行高。

请查看文档,查看虚拟化网格示例。

滚动时延迟加载

许多网站会等到用户滚动到长列表底部,然后再加载和渲染列表中的项,以此来提升性能。此技术通常称为“无限加载”,会在用户滚动到接近末尾的特定阈值后,将新的 DOM 节点添加到列表中。虽然这比一次性加载列表上的所有项要好,但如果用户滚动经过这些项,它最终仍会填充具有数千个行条目的 DOM。这可能会导致 DOM 大小过大,从而导致样式计算和 DOM 更改速度变慢,进而影响性能。

下图可以帮助您总结这一点:

常规列表与虚拟化列表之间的滚动差异
常规列表和虚拟化列表之间的滚动差异

解决此问题的最佳方法是继续使用 react-window 等库在页面上维护一个小元素“窗口”,但也要随着用户向下滚动而延迟加载较新的条目。通过单独的软件包 react-window-infinite-loaderreact-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 会解析为列表的其他数据

渲染 prop 用于返回列表组件用于渲染的函数。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 属性,以防止空内容闪烁。请勿过度扫描过多条目,否则会对性能产生负面影响。