React 性能优化技巧
在这篇文章中,我们将研究如何提高需要在屏幕上渲染大量组件的 React 应用程序的性能。
我们通常考虑在大多数应用程序中使用pagination
或virtualization
来提供更好的用户体验,并且这适用于大多数用例,但如果我们有一个用例需要在屏幕上呈现大量组件,同时又不放弃用户体验和性能,该怎么办?
为了演示,我设计了一个简单的应用,在屏幕上渲染 3 万个方块,并在用户点击这些方块时更新计数。我使用了react 17.0.0
带有钩子的函数式组件。
这是应用的预览图。它包含一个App
组件和一个Square
组件。点击方块时有明显的延迟。
// App.jsx
import React, { useState } from "react";
import Square from "./components/square/square";
const data = Array(30000)
.fill()
.map((val, index) => {
return { id: index, key: `square-${index}` };
});
const App = () => {
const [count, setCount] = useState(0);
const [items, setItems] = useState(data);
return (
<div>
<p>Count: {count}</p>
{items.map(({ key, id, clicked }) => (
<Square
key={key}
id={id}
clicked={clicked}
onClick={id => {
const newItems = [...items];
newItems[id].clicked = true;
setCount(val => val + 1);
setItems(newItems);
}}
/>
))}
</div>
);
};
export default App;
// Square.jsx
import React from "react";
import "./square.css";
const Square = ({ onClick, id, clicked }) => {
return (
<div
className={`square ${clicked && "clicked"}`}
onClick={() => onClick(id)}
/>
);
};
export default Square;
让我们在两个组件中添加控制台语句,检查它们是否出现了不必要的渲染,然后点击其中一个方块。我们发现Square
组件函数被调用了 3 万次。
另外,我们可以看到600ms
在 React Dev tools Profiler Tab 上重新渲染 UI 所花费的时间。在页面加载时开始分析 -> 单击任意方块 -> 停止分析。
我们需要避免重新渲染Square
组件,因为它的 for 属性没有任何props
变化。我们将使用React.memo
它。
什么是React.memo
?
React.memo
是一个高阶组件,它通过记住初始渲染的结果来帮助跳过重新渲染。React.memo
仅当发生变化时才重新渲染组件prop
。
注意:
React.memo
进行的是浅比较。为了更好地控制,我们可以传递如下所示的比较函数。React.memo(Component, (prevProps, nextProps) => { // return true if the props are same, this will skip re-render // return false if the props have changed, will re-render });
这是Square
带有React.memo
// Square component with React.memo
import React from "react";
import "./square.css";
const Square = ({ onClick, id, clicked }) => {
return (
<div
className={`square ${clicked && "clicked"}`}
onClick={() => onClick(id)}
/>
);
};
export default React.memo(Square);
现在让我们尝试使用如下所示的附加设置再次进行分析。
我们目前还没有看到任何变化。但是当我们将鼠标悬停在Square
组件上时,它会显示onClick
prop 已更改,从而触发了这次重新渲染。这是因为我们在每次渲染 prop 时都会传递一个新函数onClick
。为了避免这种情况,我们使用useCallback
。
什么是useCallback
?
useCallback
是一个返回记忆回调的钩子。
// App component with useCallback
import React, { useState, useCallback } from "react";
import Square from "./components/square/square";
const data = Array(30000)
.fill()
.map((val, index) => {
return { id: index, key: `square-${index}` };
});
const App = () => {
const [count, setCount] = useState(0);
const [items, setItems] = useState(data);
const onClick = useCallback(
id => {
const newItems = [...items];
newItems[id].clicked = true;
setCount(val => val + 1);
setItems(newItems);
},
[items]
);
return (
<div>
<p>Count: {count}</p>
{items.map(({ key, id, clicked }) => (
<Square key={key} id={id} clicked={clicked} onClick={onClick} />
))}
</div>
);
};
export default App;
让我们再次进行分析。我们现在避免了重新渲染Squares
,这减少了 的时间118ms
。
Square
现在我们看到了更好的性能。我们使用 memoization避免了组件的重新渲染,但React
仍然需要比较所有 3 万个元素的 props。这是我们应用的组件树。
如果您仍然发现性能问题,我们可以更进一步。组件Square
下有 3 万个元素App
。为了减少 React 比较 props 的时间,我们需要减少这一层的组件数量。这该怎么办?我们可以引入另一层组件吗?是的,我们将把 3 万个元素的列表拆分成更小的块,并使用中间组件进行渲染。
在实际应用中,我们可以找到一个合理的位置将列表拆分成更小的块。但在这里,我们先将它们拆分成每个块包含 500 个方格的块。
// App component
import React, { useState, useCallback } from "react";
import Row from "./components/row/row";
let num = 0;
const data = Array(30000)
.fill()
.map((val, index) => {
if (index % 500 === 0) {
num = 0;
}
return { id: num++, key: `square-${index}` };
});
const chunkArray = (array, chunkSize) => {
const results = [];
let index = 1;
while (array.length) {
results.push({
items: array.splice(0, chunkSize),
key: String(index)
});
index++;
}
return results;
};
const chunks = chunkArray(data, 500);
const App = () => {
const [count, setCount] = useState(0);
const [allItems, setAllItems] = useState(chunks);
const onClick = useCallback(
(id, index) => {
const chunk = [...allItems[index].items];
chunk[id].clicked = true;
setCount(val => val + 1);
allItems[index].items = chunk;
setAllItems(allItems);
},
[allItems]
);
return (
<div>
<p>Count: {count}</p>
{allItems.map(({ items, key }, index) => (
<Row items={items} onClick={onClick} key={key} index={index} />
))}
</div>
);
};
export default App;
// Row component
import React, { useCallback } from "react";
import Square from "../square/square";
const Row = ({ items, onClick, index }) => {
const onItemClick = useCallback(
id => {
onClick(id, index);
},
[onClick, index]
);
return (
<>
{items.map(({ id, key, clicked }) => (
<Square key={key} onClick={onItemClick} id={id} clicked={clicked} />
))}
</>
);
};
export default React.memo(Row);
我们再来分析一下。现在没有发现任何延迟。我们的组件数量少了很多,Row
所以 props 的比较非常快,而且如果props 没有变化, React 甚至可以跳过Square
props 的比较。Row
这是最终的应用程序
Stackblitz 预览
Stackblitz 代码
React.memo
可以useCallback
用来提升性能。这是否意味着我们应该用 包裹所有组件,用React.memo
包裹所有函数useCallback
?不,React.memo
而是useCallback
使用 memoization,这会增加内存占用,而且函数本身也需要运行时间,并且会产生诸如 prop 比较之类的开销。我们所做的拆分也会增加内存占用。
何时使用React.memo
和useCallback
?
除非您发现特定组件或整个应用存在延迟,否则无需使用它们。如果存在延迟,请尝试分析该屏幕上的操作,并检查是否有可以避免的组件重新渲染。useCallback
当我们将函数用作钩子的依赖项以避免运行不必要的代码块时,它也很有用。
结论
虽然React.memo
、useCallback
、useMemo
可以用来优化 React 应用程序的性能,但大多数情况下它们并不是必需的。请谨慎使用它们。