如何使用React Window虚拟化大型列表

2,088 阅读7分钟

在这个由JavaScript驱动的网页的现代领域,DOM可能是一个昂贵的抽象概念。如果没有合适的工具来提高性能,你的React应用中的一个道具变化就会导致元素不必要地重新渲染。

但是,即使没有JavaScript的参与,拥有一个大的DOM树也会减慢你的页面,并破坏你的核心Web Vitals,给你的网络请求、运行时间和内存性能带来负担。

关于DOM大小的标准

DOM Dashboard With 76 Performance Rating

重要的是要记住,尽管浏览器可以处理更大的DOM树,但建议将DOM节点总数限制在1500个,DOM深度限制在32个,单个父元素的DOM节点数限制在60个。

我们可以通过在电线上发送一个相当大的HTML文件,或者在运行时生成元素,直到超过性能预算,从而导致DOM尺寸过大。

使用原样、无限滚动和分页作为虚拟化的替代品

当显示一组大型数据时,我们有很多方法可以实现可视化。最值得注意的方法是通过原样、分页或无限滚动来呈现数据集。

我们可以将这三种方式可视化。

Single Column List Compared To Never Ending Column List Compared To New Page List

当我们的页面上有连续的内容,如多个段落,我们会使用 "原样 "策略来呈现我们的内容。为了优化我们的页面性能,我们求助于CSScontent-visibility 属性。更多细节请参见这篇博文

然而,使用content-visibility ,只能在最初的渲染中有所帮助。当我们向下滚动页面到浏览器跳过的渲染区域时,我们最终会再次看到一个缓慢移动的页面。

无限滚动的情况也是如此。不同的是,我们只在需要时请求内容。然而,我们最终也会遇到同样迟缓的性能问题。

另一方面,分页是最有性能的渲染方式。它在初始渲染时只显示必要的内容,根据需要请求内容,而且DOM永远不会因为不必要的内容而膨胀。

但是,分页是一种模式,并不适合在网页上显示每一个大型数据集。相反,我们可以使用虚拟化。

什么是虚拟化?

虚拟化是一个渲染概念,它专注于跟踪用户的位置,只提交在任何给定的滚动位置与DOM视觉相关的内容。从本质上讲,它为我们提供了分页的所有好处,以及无限滚动的用户体验。

Rendering And Removing Column List

为了虚拟一个列表,我们使用给定的列表项的尺寸预先计算出列表的总高度,并将其乘以列表项的数量。

然后,我们定位这些项目以创建一个用户可以滚动的列表。正确定位我们的元素是虚拟化效率的关键,因为单个项目可以被添加或删除而不影响其他项目或导致它们回流(即重新计算一个元素在页面上的位置的过程)。

然而,还有另一种方法来渲染数据。

如何用虚拟的方式来处理一个大的列表react-window

为了实现虚拟化,我们将使用 [react-windo](https://github.com/bvaughn/react-window),它是对react-virtualized 的重写。你可以在这里阅读这两个库之间的比较。

要安装react-window ,请运行以下程序。

$ yarn add react-window # the library
$ yarn add -D @types/react-window # auto-completion

react-window 将作为一个依赖项被安装,而它的类型将作为devDependency ,即使我们不使用TypeScript。我们还需要 [faker.js](https://github.com/marak/Faker.js/)来生成我们的大型数据集。

$ yarn add faker

在我们的App.js ,我们将导入faker 以及useState ,并通过faker'saddress.city 函数初始化我们的data 状态。length 在我们的代码中,它将创建一个数组,其中的10000

import React, { useState } from "react";
import * as faker from "faker";

const App = () => {
  const [data, setData] = useState(() =>
    Array.from({ length: 10000 }, faker.address.city)
  );

  return (
    <main>
      <ul style={{ width: "400px", height: "700px", overflowY: "scroll" }}>
        {data.map((city, i) => (
          <li key={i + city}>{city}</li>
        ))}
      </ul>
    </main>
  );
};

接下来,我们用一个函数懒散地初始化我们的状态,以优化性能。然后,我们通过给它一个宽度和高度并将overflowY 设置为scroll ,使我们的列表可以滚动。

为了比较有虚拟化和无虚拟化的性能,我们将添加一个reverse 按钮,将我们的data 阵列反转。

const App = () => {
  const [data, setData] = useState(() =>
    Array.from({ length: 10000 }, faker.address.city)
  );

  const reverse = () => {
    setData((data) => data.slice().reverse());
  };

  return (
    <main>
      <button onClick={reverse}>Reverse</button>
      <ul style={{ width: "400px", height: "700px", overflowY: "scroll" }}>
        {data.map((city, i) => (
          <li style={{ height: "20px" }} key={i + city}>{city}</li>
        ))}
      </ul>
    </main>
  );
};

请看CodePen上Simohamed(@smhmd)
的笔
React中的非虚拟化列表。

现在,试试这个反转按钮,注意更新是多么的潜移默化。

为了虚拟化这个列表,我们将使用react-window'sFixedSizeList

import { FixedSizeList as List } from "react-window";

const App = () => {
  const [data, setData] = useState(() =>
    Array.from({ length: 10000 }, faker.address.city)
  );
  const reverse = () => {
    setData((data) => data.slice().reverse());
  };

  return (
    <main>
      <button onClick={reverse}>Reverse</button>
      <List
        innerElementType="ul"
        itemCount={data.length}
        itemSize={20}
        height={700}
        width={400}
      >
        {({ index, style }) => {
          return (
            <li style={style}>
              {data[index]}
            </li>
          );
        }}
      </List>
    </main>
  );
};

我们可以以多种方式使用FixedSizeList 。在这个例子中,我们正在创建一个与我们的data (通过itemCount )相同长度的假想数组,并使用它来索引我们的data

FixedSizeList's children expose a render prop that has each index and the necessary styles (absolute positioning styles, etc.) passed into it.

我们也可以显式地传递我们的数据,并通过itemData ,在渲染道具中接收它,像这样。

<List
  itemData={data}
  innerElementType="ul"
  itemCount={data.length}
  itemSize={20}
  height={700}
  width={400}
>
  {({ data, index, style }) => {
    return <li style={style}>{data[index]}</li>;
  }}
</List>

注意,我们之前的内联样式现在被widthheight 道具所取代。overflowYlayout 道具控制,它默认为vertical

style 渲染道具参数传递给最外层的元素(li ,在我们的例子中)是很重要的。如果没有这个参数,所有的元素都会堆叠在一起,没有什么可以滚动的。

FixedSizeList 元素会渲染两个包装元素,它们都默认为divs,并可以使用innerElementTypeouterElementType 进行定制。

在我们的案例中,出于可访问性的考虑,我们将innerElementType 设置为ul 。然而,只有预定义的道具可以使用。添加诸如roledata-* 等道具不会有任何影响。

默认情况下,FixedSizeList 将使用数据索引作为React键。但由于我们正在修改我们的数据数组,我们必须为我们的键使用唯一的值。为此,FixedSizeList 暴露了itemKey 道具,它接受一个函数,应该返回一个字符串或一个数字。我们将使用faker'sdatatype.uuid 函数。

<List
  itemKey={faker.datatype.uuid}
  itemData={data}
  innerElementType="ul"
  itemCount={data.length}
  itemSize={20}
  height={700}
  width={400}
>
  {({ data, index, style }) => {
    return <li style={style}>{data[index]}</li>;
  }}
</List>

请看CodePen上Simohamed(@smhmd)
的Pen
React中的虚拟化列表

正如我提到的,我们可以使用反向按钮即时比较我们的虚拟化列表和非虚拟化列表。但性能的优化并没有结束。如果我们有一个昂贵的元素,我们为每个列表项渲染,而不是我们的单一lireact-window ,允许我们在滚动时渲染一个简单的UI来代替。

要做到这一点,我们首先需要通过传递useIsScrolling 到我们的FixedSizeList 来启用isScrolling 布尔值。

<List
  useIsScrolling={true}
  itemCount={data.length}
  itemSize={20}
  height={700}
  width={400}
>
  {({ index, style, isScrolling }) =>
    isScrolling ? (
      <Skeleton style={style} />
    ) : (
      <ExpensiveItem index={index} style={style} />
    )
  }
</List>;

下面是它的样子。

Delayed Rendering And Removing Data After Scrolling

请看CodePen上Simohamed(@smhmd) Pen
React Window的isScrolling

如何用网格虚拟化react-window

现在我们知道了如何虚拟一个列表,让我们来学习如何虚拟一个网格。这是一个类似的过程,但不同的是,你必须在两个方向上添加你的数据的数量和尺寸:垂直(列)和水平(行)。

import { FixedSizeGrid as Grid } from "react-window";
import * as faker from "faker";

const COLUMNS = 18;
const ROWS = 30;

const data = Array.from({ length: ROWS }, () =>
  Array.from({ length: COLUMNS }, faker.internet.avatar)
);

function App() {
  return (
    <Grid
      columnCount={COLUMNS}
      rowCount={ROWS}
      columnWidth={50}
      rowHeight={50}
      height={500}
      width={600}
    >
      {({ rowIndex, columnIndex, style }) => {
        return <img src={data\[rowIndex\][columnIndex]} alt="" />;
      }}
    </Grid>
  );
}

请看CodePen上Simohamed(@smhmd)的Pen
React窗口网格

很简单,对吗?

总结

在这篇文章中,我们介绍了DOM的性能限制,以及如何使用多种渲染策略来优化一个精简的DOM。我们还讨论了如何通过使用react-window ,虚拟化可以有效地显示大型数据集,以满足我们的性能目标。

The postHow to virtualize large lists using React Windowappeared first onLogRocket Blog.