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

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

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

这是一个使用 react-window 渲染包含 1000 行的列表的示例。尝试尽可能快地滚动。

为什么这很有用?

有时您可能需要显示包含许多行的大型表格或列表。在这种列表中加载每个项目都会严重影响性能。

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

Window of content in a virtualized list
虚拟化列表中内容“窗口”的移动

当用户向下滚动列表时,退出“窗口”的 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 还支持虚拟化多维列表或网格。在这种情况下,当用户水平垂直滚动时,可见内容的“窗口”会发生变化。

Moving window of content in a virtualized grid is two-dimensional
虚拟化网格中内容“窗口”的移动是二维的

同样,FixedSizeGridVariableSizeGrid 组件都可以使用,具体取决于特定列表项的大小是否可以变化。

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

查看文档以查看虚拟化网格的示例。

滚动时延迟加载

许多网站通过等待用户向下滚动后才加载和渲染长列表中的项目来提高性能。这种技术通常称为“无限加载”,当用户滚动到接近末尾的某个阈值时,会将新的 DOM 节点添加到列表中。虽然这比一次加载列表中的所有项目要好,但如果用户滚动超过那么多项目,它仍然会最终用数千个行条目填充 DOM。这可能会导致 DOM 大小过大,从而通过减慢样式计算和 DOM 突变来影响性能。

以下图表可能有助于总结这一点

Difference in scrolling between a regular and virtualized list
常规列表和虚拟化列表之间的滚动差异

解决此问题的最佳方法是继续使用像 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 DevTools 中的 FPS 计量器来探索项目在列表上的渲染效率。
  2. 对于任何影响性能的长列表或网格,请包含 react-window
  3. 如果 react-window 中不支持某些功能,如果您无法自行添加此功能,请考虑使用 react-virtualized
  4. 如果您需要在用户滚动时延迟加载项目,请使用 react-window-infinite-loader 包裹您的虚拟化列表。
  5. 对于您的列表,请使用 overscanCount 属性,对于您的网格,请使用 overscanColumnsCountoverscanRowsCount 属性,以防止空白内容闪烁。不要过度扫描太多条目,因为这会对性能产生负面影响。