内容来自How to useMemo and useCallback: you can remove most of them(中文版)阅读感悟与思考。
结论
useMemo
(与useCallback
)的使用有两个出发点:
- 避免组件的重复渲染。
- 缓存一些复杂逻辑的计算结果,避免重复计算。
借助useMemo
和useCallback
进行性能优化时应做到:
- 避免子组件的重渲染时需要做到:保证
React.memo
缓存了子组件,且保证子组件的每一个prop
都进行了缓存。 - 缓存计算时应该慎重考虑(严重怀疑)其必要性,尽可能减少
useMemo
的使用。
避免重复渲染
首先明确react组件重渲染的触发条件:props
或者state
发生改变触发组件重渲染;父组件的重渲染触发子组件的重渲染。
针对以上两个重渲染条件,如果想避免一个组件的无效重复渲染的话,既要保证父组件中对一些引用类型的props
进行useMemo/useCallback
缓存,还要通过React.memo
缓存组件,以告诉React,在决定组件要不要重新渲染之前对比props
是否发生了改变,如果没变,就不要重新渲染了。
正确的缓存方式,如下demo:
jsx
// React.memo缓存组件------父组件渲染触发组件渲染之前,根据prop是否改变来决定是否重新渲染
const PageMemoized = React.memo(Page);
const App = () => {
const value = useMemo(() => [1, 2, 3], []);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
// page WILL NOT re-render because value and onClick are all memoized
<PageMemoized onClick={onClick} value={value} />
);
};
缓存昂贵计算
就像React官方文档所明确的,useMemo
的初衷在于缓存一些"昂贵"的计算:
js
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
但是需要明确的一点是,js逻辑的运算速度远比我们想象的快,而且很多时候,可能我们选择去通过缓存计算进行优化时,方向可能就已经走偏了,看如下demo:
jsx
function App() {
const startTime = performance.now();
// array初始值由小到大排列
const array = new Array(250).fill().map((value, index) => index);
// 冒泡排序O(n²)复杂度排序array数组为由大到小
for(let i = 0; i < array.length; i ++) {
for(let j = 0; j < array.length - i - 1; j ++) {
if(array[j] < array[j + 1]) {
[array[j], array[j + 1]] = [array[j + 1], array[j]];
}
}
}
const endTime = performance.now();
console.log(`sort array consume ${endTime - startTime} ms`);
return (
<div>
<button>按钮文字</button>
<Measure before={endTime}/>
</div>
)
}
export default App
// Measure.jsx
export default function ({before}) {
const renderTime = performance.now();
console.log(`render a button consume ${renderTime - before} ms`);
return (
<div></div>
)
}
上面,我们用performance.now()
计算出来应用渲染过程中的两段耗时:第一段时间在App.jsx
中打印,是把一个250规模的数组进行排序,且效果最差(完全反转,耗时最多)情况的耗时;第二段我们借助一个Measure
组件计算渲染一个只有文本的原生按钮的耗时,在CPU放慢6倍的情况下,运行结果如下:
可以直观的感受到两个问题:
- js逻辑的运行速度非常快,远比我们想象得快:在6倍缓速的情况下,排序一个数组只用了20毫秒,而且250的数组规模加上低效的算法,应该堪比一些常见的业务场景了,足以说明问题
- 与渲染相比,计算的消耗非常小:如上仅仅渲染一个原生按钮,就产生了计算一半的耗时。
所以说,考虑到useMemo
进行缓存的过程本身也会在渲染初期带来一定的消耗,这必然会影响应用的首屏渲染速度,而且,缓存带来的优化是局部的,但是应用里泛滥的useMemo
在渲染时却是一个不少的产生了消耗。
useMemo清除指南
useCallback
与useMemo
只在应用后续的重新渲染中发挥一定的作用;对于初始渲染它们甚至是有害的,而且只有当每一个prop
都被缓存,且组件本身也被缓存的情况下,重渲染才能被避免。稍有不慎,前功尽弃。不如简单点,把所有对props
的缓存都删了吧。 (等到有了性能瓶颈再针对性的加)- 把包裹了"纯 js 操作"的
useMemo
也都删了吧。与组件本身的渲染相比,它缓存数据带来的耗时减少是微不足道的,并且会在初始渲染时消耗额外的内存,造成可以被观察到的延迟。