对我们的下一本书感兴趣?了解更多关于 使用 React 构建大规模 JavaScript Web 应用程序

性能模式

列表虚拟化

在本指南中,我们将讨论列表虚拟化(也称为窗口化)。 这指的是在动态列表中仅渲染可见的行,而不是整个列表。 渲染的行只是完整列表的一小部分,用户滚动时可见的内容(窗口)会移动。 这可以提高渲染性能。

如果你使用 React 并且需要**高效地显示大量数据列表**,你可能熟悉 react-virtualized。 它是由 Brian Vaughn 编写的窗口化库,仅渲染列表中当前可见的项(在滚动的“视窗”内)。 这意味着你不需要为一次渲染数千行数据付出代价。 有一个关于使用 react-window 进行列表虚拟化的视频 演练,作为本文的补充。

列表虚拟化如何工作?

未渲染

未渲染

已渲染

已渲染

已渲染

已渲染

未渲染

未渲染

未渲染

未渲染

<ul>

“虚拟化”一项列表涉及**维护一个窗口**并**在你的列表周围移动该窗口**。 react-virtualized 中的窗口化通过以下方式工作:

  • 拥有一个小的容器 DOM 元素(例如 <ul>),具有相对定位(窗口)
  • 拥有一个用于滚动的大的 DOM 元素
  • 在容器内绝对定位子元素,设置其顶部、左侧、宽度和高度样式。

而不是一次渲染列表中的 1000 个元素(这会导致初始渲染速度变慢或影响滚动性能),**虚拟化专注于仅渲染对用户可见的项**。

Impact of virtualization leading to a faster frame-rate vs rendering all at once

这有助于在中低端设备上保持列表渲染速度。 你可以在用户滚动时获取/显示更多项目,卸载之前的条目并用新的条目替换它们。

react-virtualized 的一个更小的替代方案

react-window 是由同一作者重写的 react-virtualized,旨在**更小**、更快,更易于 树摇

Bundlephobia showing a 34KB gzipped size for react-virtualized vs 5KB for react-window

在树摇库中,大小是使用 API 表面的函数。 我发现使用它代替 react-virtualized 可以节省大约 20-30KB(压缩)的空间。

Webpack bundle analyzer showing a ~20KB size difference

这两个软件包的 API 类似,不同之处在于 react-window 往往更简单。 react-window 的组件包括

列表

列表渲染**窗口化的列表(行)元素**,这意味着仅对用户显示可见的行(例如 FixedSizeListVariableSizeList)。 列表使用网格(内部)来渲染行,并将 props 传递给该内部网格。

未渲染

未渲染

使用 React 渲染数据列表

以下是如何使用 React 渲染简单数据 (itemsArray) 列表的示例

import React from "react";
import ReactDOM from "react-dom";

const itemsArray = [
  { name: "Drake" },
  { name: "Halsey" },
  { name: "Camillo Cabello" },
  { name: "Travis Scott" },
  { name: "Bazzi" },
  { name: "Flume" },
  { name: "Nicki Minaj" },
  { name: "Kodak Black" },
  { name: "Tyga" },
  { name: "Buno Mars" },
  { name: "Lil Wayne" }, ...
]; // our data

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <div
    style={{
      height: 150,
      width: 300
    }}
    class="List"
  >
    {itemsArray.map((item, index) => Row({ index }))}
  </div>
);

ReactDOM.render(<Example />, document.getElementById("root"));

使用 react-window 渲染列表

…以下是使用 react-window 的 FixedSizeList 的相同示例,它需要一些 props (widthheightitemCountitemSize) 以及作为子元素传递的行渲染函数

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

const itemsArray = [...]; // our data

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <List
    className="List"
    height={150}
    itemCount={itemsArray.length}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

ReactDOM.render(<Example />, document.getElementById("root"));

你可以在 CodeSandbox 上尝试 FixedSizeList

网格

网格渲染**表格数据**,并在垂直和水平轴上进行虚拟化(例如 FizedSizeGridVariableSizeGid)。 它仅渲染根据当前水平/垂直滚动位置填充自身的网格单元格。

单元格

单元格

单元格

单元格

单元格

单元格

单元格

单元格

单元格

未渲染

未渲染

未渲染

未渲染

未渲染

未渲染

未渲染

如果我们想用网格布局渲染与之前相同的列表,假设我们的输入是一个多维数组,我们可以使用 FixedSizeGrid 来完成,如下所示

import React from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeGrid as Grid } from 'react-window';

const itemsArray = [
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
];

const Cell = ({ columnIndex, rowIndex, style }) => (
  <div
    className={
      columnIndex % 2
        ? rowIndex % 2 === 0
          ? 'GridItemOdd'
          : 'GridItemEven'
        : rowIndex % 2
          ? 'GridItemOdd'
          : 'GridItemEven'
    }
    style={style}
  >
    {itemsArray[rowIndex][columnIndex].name}
  </div>
);

const Example = () => (
  <Grid
    className="Grid"
    columnCount={5}
    columnWidth={100}
    height={150}
    rowCount={5}
    rowHeight={35}
    width={300}
  >
    {Cell}
  </Grid>
);

ReactDOM.render(<Example />, document.getElementById('root'));

你也可以在 CodeSandbox 上尝试 FixedSizeGrid

更深入的 react-window 示例

Scott Taylor 使用 react-windowFixedSizeGrid 实现了一个开源的 Pitchfork 音乐评论抓取器 (src)。 以下是该应用程序运行的视频

Pitchfork 抓取器使用 react-window-infinite-loader (demo),它有助于将大型数据集分解成块,这些块可以在滚动到视图中时加载。

以下是 react-window-infinite-loader 如何在该应用程序中使用的示例

import React, { Component } from 'react';
import { FixedSizeGrid as Grid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
...
  render() {
    return (
      <InfiniteLoader
        isItemLoaded={this.isItemLoaded}
        loadMoreItems={this.loadMoreItems}
        itemCount={this.state.count + 1}
      >
        {({ onItemsRendered, ref }) => (
          <Grid
            onItemsRendered={this.onItemsRendered(onItemsRendered)}
            columnCount={COLUMN_SIZE}
            columnWidth={180}
            height={800}
            rowCount={Math.max(this.state.count / COLUMN_SIZE)}
            rowHeight={220}
            width={1024}
            ref={ref}
          >
            {this.renderCell}
          </Grid>
        )}
      </InfiniteLoader>
    );
  }
}

你可能会发现将应用程序从 react-virtualized 移植到 react-windowcommit 很有用。

使用 FixedSizeList 实现的 Pitchfork 抓取器也可用(demoPixel 上的 demo

以下是如何实现该应用程序的示例

return (
  <InfiniteLoader
    isItemLoaded={this.isItemLoaded}
    loadMoreItems={this.loadMoreItems}
    itemCount={this.state.count}
  >
    {({ onItemsRendered, ref }) => (
      <section>
        <FixedSizeList
          itemCount={this.state.count}
          itemSize={ROW_HEIGHT}
          onItemsRendered={onItemsRendered}
          height={this.state.height}
          width={this.state.width}
          ref={ref}
        >
          {this.renderCell}
        </FixedSizeList>
      </section>
    )}
  </InfiniteLoader>
);

如果我们需要更复杂的网格虚拟化解决方案呢? 我们发现了一个 The Movie Database 演示应用程序,它在后台使用了 react-virtualized 和 Infinite Loader。

移植 到 react-window 和 react-window-infinite-loader 并不会花费太长时间,但我们确实发现了一些组件尚未得到支持。 不管怎样,最终的功能非常接近

TMDB Viewer

缺少的组件是 WindowScroller 和 AutoSizer…我们将在接下来讨论它们。

...
    return (
      <section>
        <AutoSizer disableHeight>
          {({width}) => {
            const {movies, hasMore} = this.props;
            const rowCount = getRowsAmount(width, movies.length, hasMore);
            ...
            return (
              <InfiniteLoader
                ref={this.infiniteLoaderRef}
                ...
                {({onRowsRendered, registerChild}) => (
                  <WindowScroller>
                    {({height, scrollTop}) => (

react-window 缺少什么?

react-window 还没有 react-virtualized 的完整 API 表面,所以如果你正在考虑使用它,请查看 比较文档。 缺少什么?

  • WindowScroller - 这是一个 react-virtualized 组件,它允许列表根据窗口的滚动位置进行滚动。 目前没有计划为 react-window 实现它,因此你需要在用户层解决它。
  • AutoSizer - HOC 会扩展以适应所有可用空间,自动调整单个子元素的宽度和高度。 Brian 将其实现为一个独立 软件包。 关注此问题以获取最新信息。
  • CellMeasurer - HOC 通过以用户不可见的方式渲染单元格内容来自动测量单元格的内容。 关注这里,了解关于支持的讨论。

也就是说,我们发现 react-window 足够满足我们大多数需求,因为它开箱即用。

Web 平台的改进

一些现代浏览器现在支持 CSS content-visibilitycontent-visibility:auto 允许你跳过渲染和绘制屏幕外内容,直到需要为止。 如果你有一个带有昂贵渲染的长的 HTML 文档,请考虑尝试使用此属性。

对于渲染动态内容列表,我仍然建议使用 react-window 这样的库。 很难有一个 content-visbility:hidden 版本的库,它能够胜过积极使用 display:none 或在屏幕外时删除 DOM 节点的版本,就像许多列表虚拟化库今天可能做的那样。

进一步阅读

要进一步了解 react-window 和 react-virtualized,请查看