原文作者:Andrew Evans
原文地址:blog.logrocket.com/react-useme…
翻译:一川
写在前面
随着HTML
页面的大小和复杂性的增长,创建高效的 React 代码变得比以往任何时候都更加重要。重新呈现大型组件的成本很高,并且通过单页应用程序 (SPA)
为浏览器提供大量工作会增加处理时间,并可能赶走用户。React
提供内置的 API 功能,通过避免不必要的重新渲染、缓存重复的昂贵操作、延迟加载组件等来提高应用程序性能。
本教程测试了两个不同的 React 钩子:useMemo
和 useCallback
。这些 Hook
可帮助开发人员提高组件的渲染性能,在React
组件渲染之间保留对象,并帮助提高应用程序性能。
React函数的性能优化
React
已经提供了 React.memo()
以避免在不更改 props
时重新创建DOM
元素。此方法是记忆最后一个结果的高阶函数(HOC)
。但是,它不记住典型的JavaScript
函数。因此,尽管在 JavaScript
中是一等公民,但每次使用时都可能会重新创建函数。
useMemo
和 useCallback
方法有助于避免在某些情况下重新创建或重新运行函数。虽然并不总是有用,但在处理大量数据或许多共享行为的组件时,useMemo
或 useCallback
可能会产生明显的差异。例如,当您创建股票或数字货币交易平台时,这将特别有用。
什么是useCallback?
当React
重新渲染组件时,组件内的函数引用会重新创建。如果通过props
将回调函数传递给 memoized ( React.memo )
子组件,即使父组件显然没有更改子组件的 props
,它也可能会重新渲染。每个父组件的重新呈现阶段都会为回调创建新的函数引用,所以不相等的回调props
会触发不需要的子组件重新呈现,即使是可见的props
也不会改变。
React Hook useCallback
返回一个基于函数和依赖项数组的memoized函数引用。我们可以用它来创建优化的回调不会导致不必要的重新渲染。如果依赖关系没有改变,这个钩子返回一个缓存(记忆)的函数引用。
useCallback 的实际回调示例
现在,让我们开始吧。首先,使用创建 React 应用程序在您的计算机上创建一个 React 项目,以跟进:
$ npx create-react-app PerformanceHooks
现在,查看以下示例源代码,该源代码将回调函数传递给记忆的子组件:
import { memo, useState } from "react";
import "./styles.css";
const Numbers = memo(({ nums, addRandom }) => {
console.log("Numbers rendered");
return (
<div>
<ul>
{nums.map((num, i) => (
<li key={i}>{num}</li>
))}
</ul>
<button onClick={addRandom}>Add random</button>
</div>
);
});
export default function App() {
const [nums, setNums] = useState([]);
const [count, setCount] = useState(0);
const increaseCounter = () => {
setCount(count + 1);
};
const addRandom = () => {
let randNum = parseInt(Math.random() * 1000, 10);
setNums([...nums, randNum]);
};
return (
<div>
<div>
Count: {count}
<button onClick={increaseCounter}>+</button>
</div>
<hr />
<Numbers nums={nums} addRandom={addRandom} />
</div>
);
}
上面的源代码在 App 组件中呈现一个计数器,在 Numbers
组件中呈现一个随机数列表。当计数器增加时,React
会重新渲染Numbers
组件,即使它使用 memo
进行了优化,如下所示:
原因是每当 App 组件重新呈现时,它都会为addRandom
回调重新创建函数引用。由于道具不同,因此 Numbers
组件被重新渲染!作为解决方案,我们可以用 useCallback
包装 addRandom
,如以下代码片段所示:
const addRandom = useCallback(() => {
let randNum = parseInt(Math.random() * 1000, 10);
setNums([...nums, randNum]);
}, [nums]);
上述解决方案消除了前面讨论的不需要的重新渲染,因为 addRandom
接收缓存的函数引用。
使用 useCallback 的利弊
请参阅下表以了解在 React 应用程序中使用 useCallback 作为性能增强的利弊:
什么是useMemo?
在某些情况下,我们必须在React
组件中包含复杂的计算。这些复杂的计算是不可避免的,可能会稍微减慢渲染流程。如果必须重新呈现处理成本高昂计算的组件以更新另一个视图结果(而不是成本高昂的计算结果),则可能会再次触发昂贵的计算,最终导致性能问题。这种情况可以通过缓存复杂的计算结果来解决。
useMemo
Hook 的作用与 useCallback
类似,但它返回的是记忆值而不是函数引用。这使您可以避免在必要时重复执行可能代价高昂的操作。 useMemo
Hook 通常返回缓存值,直到依赖项发生更改。如果依赖项被更改,React 将重新进行昂贵的计算并更新记忆值。
useMemo 的实际示例
查看以下源代码,该代码使用一个状态字段进行昂贵的计算:
import { useState } from "react";
import "./styles.css";
export default function App() {
const [nums, setNums] = useState([]);
const [count, setCount] = useState(1);
const increaseCounter = () => {
setCount(count + 1);
};
const addRandom = () => {
let randNum = parseInt(Math.random() * 1000, 10);
setNums([...nums, randNum]);
};
const magicNum = calculateMagicNumber(count);
return (
<div>
<div>
Counter: {count} | Magic number: {magicNum}
<button onClick={increaseCounter}>+</button>
</div>
<hr />
<div>
<ul>
{nums.map((num, i) => (
<li key={i}>{num}</li>
))}
</ul>
<button onClick={addRandom}>Add random</button>
</div>
</div>
);
}
function calculateMagicNumber(n) {
console.log("Costly calculation triggered.");
let num = 1;
for (let i = 0; i < n + 1000000000; i++) {
num += 123000;
}
return parseInt(num - num * 0.22, 10);
}
上面的代码在应用中实现了两个主要功能片段:
- 计数器值、幻数和计数器增量按钮
- 带有添加更多随机数按钮的随机数列表
运行应用后,它将按预期工作。它将计算当前计数器值的幻数,并在您单击“添加随机”按钮时将新的随机数添加到列表中。幻数计算成本很高,因此一旦增加计数器值,您就会感到延迟。
有一个隐藏的问题。为什么添加随机按钮的工作速度与幻数生成过程一样慢?查看以下预览:
原因是添加随机按钮还会触发强制重新渲染,从而触发 calculateMagicNumber
慢功能。作为解决方案,我们可以将 calculateMagicNumber
函数调用与useMemo
包装在一起,让 React
在 App
组件通过 Addrandom
按钮重新渲染时使用缓存值:
const magicNum = useMemo(() => calculateMagicNumber(count), [count]);
现在,useMemo
Hook 只有在 count 依赖项发生变化时才计算一个新的幻数,因此 Add 随机将更快地工作!
使用 useMemo 的利弊
请参阅下表以了解在 React 应用程序中使用 useMemo 作为性能增强的利弊:
useMemo 优点 |
useMemo 缺点 |
---|---|
帮助开发人员缓存值以避免不必要的昂贵重新计算 | 为计算函数调用添加过多的语法,因此使用 useMemo 可能会使代码复杂化 |
当子组件使用不需要频繁重新计算的计算对象时,能够与内置 memo 一起使用 | 可用于缓存函数,但会影响可读性(使用 useCallback 表示缓存函数) |
作为一个内置的、稳定的 React 核心功能,我们可以在生产中使用 | React 新手可能会在不需要缓存的情况下使用此 Hook,例如简单的计算、频繁更改的值等。因此,代码可读性和应用内存使用将受到影响 |
提供一个简单的函数接口,只接受两个参数:函数和依赖关系数组 | 过度使用可能会导致与内存相关的性能问题 |
useMemo 和 useCallback 中的参照相等
一个 React 库通常需要检查两个标识符的相等性。例如,当你更新组件状态字段时,React 需要在触发新的重新渲染之前检查以前的状态字段是否不等于当前状态字段。同样, useMemo
和 useCallback
需要检查标识符的相等性,以使缓存项无效。
在 JavaScript
中,基元的相等性检查很简单,因为它们是不可变的(如果不创建新基元就无法更改)。但是,对象和函数是可变的。如果 React 对对象和函数使用深度比较,就会有性能缺陷。因此,React 使用 JavaScript
的引用相等概念来比较两个标识符。
参照相等是指根据标识符的引用来比较标识符。例如,下面的代码段打印 true 两次,因为对象和函数引用相等:
let a = {msg: 'Hello'};
let b = a;
b.msg = 'World';
console.log(a === b); // true
let c = () => {};
let d = c;
console.log(c === d); // true
以下代码段打印 false 两次,即使标识符数据看起来相同:
let a = {msg: 'Hello'};
let b = {msg: 'Hello'};
console.log(a === b); // false
let c = () => 100;
let d = () => 100;
console.log(c === d); // false
上面的代码片段打印 false 两次,因为标识符引用不同。React
在内部通过 Object.is()
方法使用这种引用相等性检查(与我们之前测试的 === 相同)来检测状态之间的变化。
当我们不使用 useCallback
时,React
触发了不需要的重新渲染,因为引用相等性是 false ,因为回调会在每次重新渲染中重新创建。类似地,如果我们通过一个没有 useMemo 的昂贵函数在组件中创建一个复杂对象,它将减慢所有重新渲染的速度,因为引用相等变为 false 。 useMemo
和 useCallback
都允许您通过返回缓存的未更改引用将引用质量设置为 true 。
在 React 中使用 useCallback 与 useMemo
useCallback
和useMemo
钩子在表面上看起来相似。但是,每种都有特定的用例。
在以下情况下,将函数包装为 useCallback
:
React.memo()
包装组件接受回调函数作为prop- 将回调函数作为依赖项传递给其他 Hook(即
useEffect
)
在以下情况下使用 useMemo
:
- 对于在发生不相关的重新渲染时应缓存的昂贵计算
- 记住另一个钩子的依赖关系
当每次调用都会重新编译代码时,回调效果很好。记忆结果有助于降低在输入随时间逐渐变化时重复调用函数的成本。另一方面,在交易示例中,我们可能不想记住不断变化的订单簿的结果。
如何使用useMemo
和useCallback
提高 React 性能
如果您的 React 应用程序触发了过多的、不需要的重新渲染,并且在每次重新渲染之前处理速度较慢,则可能会使用更多的 CPU 和内存。对于通过小应用的用户来说,这种情况不会引起注意。但是,存在严重呈现性能问题的大型应用可能会降低用户计算机的速度,从而降低可用性因素和产品质量。React 可帮助您解决与 useMemo
和 useCallback
相关的关键渲染性能问题。
以下是您可以使用 useMemo
和 useCallback
提高 React 应用程序性能的清单:
- 首先,您需要接受存在性能问题的事实。通过代码查看它(通过进行个人代码评审或同行评审)或从您的应用程序中感受它
- 使用
Chrome DevTools
、console.time()
或React Profiler
测量实际渲染性能和代码执行时间。检测不需要的重新渲染 - 决定并使用
useMemo
或useCallback
- 使用这些与性能相关的钩子并再次测量性能
过度使用 useMemo
和useCallback
可能会使现有的性能问题恶化,因此让我们在下面解释反模式。
useCallback
和 useMemo
反模式
人们很容易认为您可以为每个函数使用 useCallback
或 useMemo
。然而,事实并非如此。存在与包装函数相关的开销。每次调用都需要额外的工作来解开函数调用并决定如何继续。
请注意, increaseCounter
函数不是我们需要添加 useCallback
的回调,而 addRandom
函数是。 increaseCounter
函数不符合我们的标准,因为它只创建一次,并且永远不会与子组件共享。
相反, Number
组件中的每个更改列表项都使用addRandom
函数。类似地,我们使用useMemo
包装 calculateMagicNumber
函数调用,但永远不会用useMemo
包装处理频繁变化数据的函数。
写在最后
useCallback
和useMemo
函数是微调 React 的工具。了解如何以及何时使用每种方法可能会提高应用程序性能。尽管如此,没有内置的性能改进 Hook 可以替代编写不佳的 React 应用程序代码库。在这里,我们提供了了解如何使用这些工具的指南,但请记住,使用它们是有代价的(缓存的内存使用量)。
一川说
觉得文章不错的读者,不妨点个关注,收藏起来上班摸鱼的时候品尝。
欢迎关注笔者公众号「宇宙一码平川」,助你技术路上一码平川。