性能模式
列表虚拟化
在本指南中,我们将讨论列表虚拟化(也称为窗口化)。 这指的是在动态列表中仅渲染可见的行,而不是整个列表。 渲染的行只是完整列表的一小部分,用户滚动时可见的内容(窗口)会移动。 这可以提高渲染性能。
如果你使用 React 并且需要**高效地显示大量数据列表**,你可能熟悉 react-virtualized。 它是由 Brian Vaughn 编写的窗口化库,仅渲染列表中当前可见的项(在滚动的“视窗”内)。 这意味着你不需要为一次渲染数千行数据付出代价。 有一个关于使用 react-window 进行列表虚拟化的视频 演练,作为本文的补充。
列表虚拟化如何工作?
未渲染
未渲染
已渲染
已渲染
已渲染
已渲染
未渲染
未渲染
未渲染
未渲染
<ul>
“虚拟化”一项列表涉及**维护一个窗口**并**在你的列表周围移动该窗口**。 react-virtualized 中的窗口化通过以下方式工作:
- 拥有一个小的容器 DOM 元素(例如
<ul>
),具有相对定位(窗口) - 拥有一个用于滚动的大的 DOM 元素
- 在容器内绝对定位子元素,设置其顶部、左侧、宽度和高度样式。
而不是一次渲染列表中的 1000 个元素(这会导致初始渲染速度变慢或影响滚动性能),**虚拟化专注于仅渲染对用户可见的项**。
这有助于在中低端设备上保持列表渲染速度。 你可以在用户滚动时获取/显示更多项目,卸载之前的条目并用新的条目替换它们。
react-virtualized 的一个更小的替代方案
react-window 是由同一作者重写的 react-virtualized,旨在**更小**、更快,更易于 树摇。
在树摇库中,大小是使用 API 表面的函数。 我发现使用它代替 react-virtualized 可以节省大约 20-30KB(压缩)的空间。
这两个软件包的 API 类似,不同之处在于 react-window 往往更简单。 react-window 的组件包括
列表
列表渲染**窗口化的列表(行)元素**,这意味着仅对用户显示可见的行(例如 FixedSizeList,VariableSizeList)。 列表使用网格(内部)来渲染行,并将 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 (width
、height
、itemCount
、itemSize
) 以及作为子元素传递的行渲染函数
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
。
网格
网格渲染**表格数据**,并在垂直和水平轴上进行虚拟化(例如 FizedSizeGrid,VariableSizeGid)。 它仅渲染根据当前水平/垂直滚动位置填充自身的网格单元格。
单元格
单元格
单元格
单元格
单元格
单元格
单元格
单元格
单元格
未渲染
未渲染
未渲染
未渲染
未渲染
未渲染
未渲染
如果我们想用网格布局渲染与之前相同的列表,假设我们的输入是一个多维数组,我们可以使用 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-window
和 FixedSizeGrid
实现了一个开源的 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-window
的 commit 很有用。
使用 FixedSizeList
实现的 Pitchfork 抓取器也可用(demo,Pixel 上的 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 并不会花费太长时间,但我们确实发现了一些组件尚未得到支持。 不管怎样,最终的功能非常接近。
缺少的组件是 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-visibility。 content-visibility:auto
允许你跳过渲染和绘制屏幕外内容,直到需要为止。 如果你有一个带有昂贵渲染的长的 HTML 文档,请考虑尝试使用此属性。
对于渲染动态内容列表,我仍然建议使用 react-window 这样的库。 很难有一个 content-visbility:hidden
版本的库,它能够胜过积极使用 display:none
或在屏幕外时删除 DOM 节点的版本,就像许多列表虚拟化库今天可能做的那样。
进一步阅读
要进一步了解 react-window 和 react-virtualized,请查看