在这个由JavaScript驱动的网页的现代领域,DOM可能是一个昂贵的抽象概念。如果没有合适的工具来提高性能,你的React应用中的一个道具变化就会导致元素不必要地重新渲染。
但是,即使没有JavaScript的参与,拥有一个大的DOM树也会减慢你的页面,并破坏你的核心Web Vitals,给你的网络请求、运行时间和内存性能带来负担。
关于DOM大小的标准
重要的是要记住,尽管浏览器可以处理更大的DOM树,但建议将DOM节点总数限制在1500个,DOM深度限制在32个,单个父元素的DOM节点数限制在60个。
我们可以通过在电线上发送一个相当大的HTML文件,或者在运行时生成元素,直到超过性能预算,从而导致DOM尺寸过大。
使用原样、无限滚动和分页作为虚拟化的替代品
当显示一组大型数据时,我们有很多方法可以实现可视化。最值得注意的方法是通过原样、分页或无限滚动来呈现数据集。
我们可以将这三种方式可视化。
当我们的页面上有连续的内容,如多个段落,我们会使用 "原样 "策略来呈现我们的内容。为了优化我们的页面性能,我们求助于CSScontent-visibility
属性。更多细节请参见这篇博文。
然而,使用content-visibility
,只能在最初的渲染中有所帮助。当我们向下滚动页面到浏览器跳过的渲染区域时,我们最终会再次看到一个缓慢移动的页面。
无限滚动的情况也是如此。不同的是,我们只在需要时请求内容。然而,我们最终也会遇到同样迟缓的性能问题。
另一方面,分页是最有性能的渲染方式。它在初始渲染时只显示必要的内容,根据需要请求内容,而且DOM永远不会因为不必要的内容而膨胀。
但是,分页是一种模式,并不适合在网页上显示每一个大型数据集。相反,我们可以使用虚拟化。
什么是虚拟化?
虚拟化是一个渲染概念,它专注于跟踪用户的位置,只提交在任何给定的滚动位置与DOM视觉相关的内容。从本质上讲,它为我们提供了分页的所有好处,以及无限滚动的用户体验。
为了虚拟一个列表,我们使用给定的列表项的尺寸预先计算出列表的总高度,并将其乘以列表项的数量。
然后,我们定位这些项目以创建一个用户可以滚动的列表。正确定位我们的元素是虚拟化效率的关键,因为单个项目可以被添加或删除而不影响其他项目或导致它们回流(即重新计算一个元素在页面上的位置的过程)。
然而,还有另一种方法来渲染数据。
如何用虚拟的方式来处理一个大的列表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>
注意,我们之前的内联样式现在被width
和height
道具所取代。overflowY
由layout
道具控制,它默认为vertical
。
将style
渲染道具参数传递给最外层的元素(li
,在我们的例子中)是很重要的。如果没有这个参数,所有的元素都会堆叠在一起,没有什么可以滚动的。
FixedSizeList
元素会渲染两个包装元素,它们都默认为div
s,并可以使用innerElementType
和outerElementType
进行定制。
在我们的案例中,出于可访问性的考虑,我们将innerElementType
设置为ul
。然而,只有预定义的道具可以使用。添加诸如role
或data-*
等道具不会有任何影响。
默认情况下,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中的虚拟化列表。
正如我提到的,我们可以使用反向按钮即时比较我们的虚拟化列表和非虚拟化列表。但性能的优化并没有结束。如果我们有一个昂贵的元素,我们为每个列表项渲染,而不是我们的单一li
,react-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>;
下面是它的样子。
请看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.