React性能优化(三):使用性能优化API 🚀
大家好,我是疯狂的小波。在前面2节中,我们介绍了一文看懂React优化原理及方案、以及第一种性能优化方案将可变部分与不变部分分离。
这一节,我们就来看看第二种优化方案:使用性能优化API。
为什么要使用性能优化API?
还记得我们在之前的示例吗?
function SelectCar() {
const [brand, setBrand] = useState({});
const [count, setCount] = useState(0);
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>修改count</button>
<BrandList brand={brand} />
<Car />
</>
);
}
上述代码中,当点击button
时count
值+1
,SelectCar
组件重新渲染;子组件 BrandList
、Car
也会重新渲染一次,哪怕此时子组件数据没有任何变化。如果这2个组件内部,还有子组件,也会全部重新渲染。
这种可以使用 将可变部分与不变部分分离 的方案进行优化。但是有时我们无法将所有内容都进行分离。那就必需要使用其他的方案了。
先来思考下,为什么会出现这种问题?是 React 的 bug 吗?
为什么出现性能问题?
在上一节中,我们提到过,只有当组件的state数据
(props
、state
、context
)变更时,组件才会重新渲染。而上面的 count
数据变更时,BrandList
、Car
组件的state数据
并没有变更,为什么也会重新渲染?
这是因为在 React
内部,props
判断是否变更默认是使用的全等比较。而每次父组件重新渲染时,子组件的 props
都是生成的一个新对象,全等判断前后 props
不相等。所以导致父组件每次更新,子组件也会重新渲染。
可以在数据变更时,进行源代码调试,在
react-dom.development.js
的beginWork
函数中添加断点,当第一个参数current
指向指定组件时。可以看下其中oldProps
与newProps
的对比。内部是通过oldProps !== newProps
进行全等判断。所以哪怕上面的Car
组件没有任何props
属性,props
也是一个空对象,对比前后props
时,{} !== {}
,不相等,重新渲染。
那怎么解决这种情况呢?那就是将 props
对象的全等比较,修改为 props
中属性值的比较。 这样我们就能够控制这种非必要的更新了。
// 修改前
oldProps !== newProps
// 修改后
oldProps.key !== newProps.key
解决方案:使用 React.memo 包裹组件
const BrandList: React.FC = props => {
// ...
}
export default React.memo(BrandList);
React.memo
包裹的组件,props
对比时会做浅比较,比较其中属性的值是否相等。如果前后 props
中属性值相等,就不会更新组件。这是避免组件重复渲染最常用的优化手段。
在上面的例子中,count
再次变更时,BrandList
组件也不会再重新渲染。因为这个时候对比 props
是对比 brand
属性值是否相等,很明显是没有变更的,所以最终判断我们的组件无变更,不重新渲染,BrandList
内部的子组件也不会重新渲染。如果我们的 props
有多个属性,则会依次进行对比,只要有任一不想等,则会重新渲染。
所以,其实我们每个组件都可以用 memo
包裹,来提高我们页面的更新性能(除非组件非常的小,重新渲染的损耗可以忽略不计)。或者针对项目中结构比较复杂、性能损耗比较严重的子树针对性进行性能优化。
一、使用 memo 包裹组件需要注意的问题
1.引用数据类型的props,修改属性值未修改引用地址 - 组件不更新
function SelectCar() {
const [brand, setBrand] = useState({
name: '宝马',
detail: {
carName: "530",
code: 2
},
info: {}
});
const changeBrand = () => {
const newBrand = {...brand}
newBrand.detail.code = 3
setBrand(newBrand)
}
return (
<>
<button onClick={changeBrand}>修改品牌</button>
<BrandList brandDetail={brand.detail} />
</>
);
}
我们修改一下之前的例子,BrandList
组件现在接收brand.detail
的prop
。
当我们点击修改品牌按钮,修改了detail
内部属性code
,并将整个brand
对象重新赋值,SelectCar
组件会重新渲染,但是发现 BrandList
组件并不会重新渲染,内部的code
属性还是2
。这是因为 brand.detail
的值并没有变,还是之前的引用地址,组件内部做浅比较的时候会判定前后 props
相等,不进行更新。
这个就是我们在使用 memo
的过程中,比较容易忽略的引用类型的更新问题。
而针对这个问题,常见的解决方案有:
-
如果对象只有一个层级:可以简单的使用浅拷贝,如展开运算符或
Object.assign
将对象重新赋值 -
如果对象层级是多层:可以使用
immutable
数据
immutable
简介
immutable
数据也被称为不可变数据,内部采用是多叉树的结构,凡是有节点被改变,那么它和与它相关的所有上级节点都更新。如果是没有关联的其他同级节点,并不会更新。
通俗点理解,immutable
对象的数据变更,会自动修改相关对象的引用地址,不相关的则不会修改。
像上例中,当brand
对象使用immutable
数据时,brand.detail.code
变更,会自动修改brand
、brand.detail
的引用地址,brand.info
则不会修改。
可能我们会觉得,直接深拷贝 brand
属性,这样就能确保所有子级都会更新了。虽然这样可以达到更新的目的,但是如果当brand
属性有其他节点(如brand.info
)并且传递给其他组件时,那也会导致这些组件被动更新。这样违背了我们使用memo
最初的目的:只更新需要更新的组件。
所以,采用 immutable
能够最大效率地更新数据结构,并且同时满足我们 memo
浅比较的需求,目前来说是比较好的方案。唯一的缺点就是使用上麻烦一点,immutable
数据需要和js
数据进行转换。
2.容易忽略的props中的函数
还是和上面demo一样,BrandList
组件内部使用memo
,这次组件额外接收了一个函数类型的prop
。
function SelectCar() {
const [brand, setBrand] = useState({});
const [count, setCount] = useState(0);
const onSelect = () => {
// ...
}
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>修改count</button>
<BrandList brand={brand} onSelect={onSelect} />
</>
);
}
此时,当我们的 count
属性变更时,发现 BrandList
组件还是会重新渲染。但是BrandList
的brand
和onSelect
属性好像都没有变啊。
这是因为 组件重新渲染时,组件内的函数也会被重新创建。
上例中count
变更时,SelectCar
组件重新渲染,onSelect
函数又会重新进行赋值,虽然内容没有变,但是函数是新创建的,引用地址已经变了。所以导致 BrandList
判定前后 props
不一致,进行更新。
这里其实 onSelect
函数是完全没有变化的。我们期望这种场景下 BrandList
也不需要更新。
此时,可以使用 useCallBack
对我们的函数进行缓存。
const onSelect = useCallback(() => {
// ...
}, [])
这样,当我们的 count
再次变更时,BrandList
组件也不会更新了。
如果我们的 onSelect
函数内部依赖页面数据,也可以将依赖项添加到依赖数组中。
const onSelect = useCallback(() => {
// ...
}, [brand])
这样,只有当我们的 brand
属性变更时,函数才会重新生成。
但是在部分场景下,如果依赖项频繁变化,那优化的效果可能就不明显了。不过后续React会出一个新的hook:
useEvent
,目前处于 RFC 阶段。它既能够保证缓存的函数引用始终是同一个,又能保证函数内每次都能拿到最新的、正确的 state,可以完美解决这个问题。
二、延伸思考:是否所有的方法都需要添加 useCallback
进行缓存,以提升性能?
否。useCallback
的原理都是 利用闭包缓存上次结果
,会有额外的内存与比较逻辑。
并不是绝对优化,而是一种成本交换,并非适用所有场景。所以应该根据实际场景来判断是否使用。
何时使用:
- 会影响到子组件的非必要更新时(如上)
- 当计算过程或函数体足够复杂时
何时不使用:
- 组件只会渲染一次时
- 依赖项频繁变动时(
useEvent
更合适)
对于useMemo
的使用场景也是和useCallback
类似
当计算过程足够复杂时,比如特别大数据的处理,复杂度较高的计算(多层嵌套循环等),可以使用进行缓存优化。
useMemo
常用的2个场景
- 缓存计算结果
const sum = useMemo(() => {
let _sum = 0;
for (let i = 1; i <= target; i++) {
_sum += i;
}
return _sum;
}, [target]);
这个示例中,当组件重新渲染时:target
属性未发生变化,则直接使用上次计算的结果;否则重新执行计算函数。
- 缓存DOM节点或组件
const child1 = useMemo(() => <Child1 a={a} />, [a]);
return (
<>
{child1}
</>
)
比如页面中一个长列表的渲染,就可以使用 useMemo
缓存列表渲染结果。
当用于缓存组件时,和 React.memo
类似,不过更推荐使用 React.memo
,代码可读性会更好。
class组件中实现该性能优化
使用 PureComponent
,会判断 class
组件的 state
和 props
属性是否有变更,也是浅比较,如果都没有变更则不重新渲染。
与使用 shouldComponentUpdate
生命周期效果一样,只是更方便。
class App extends React.PureComponent {
}
总结
当父组件数据变更时,会导致所有子孙组件全部重新渲染,造成大量的性能损耗。
此时我们可以通过使用 React.memo
包裹组件,来达到性能优化,这样只有当 props
内部属性值变更时,组件才会重新渲染。
使用 React.memo
进行优化时,需要注意2个点:
1、引用数据类型的prop
,直接修改内部值未修改引用地址时组件不会更新。此时可以根据情况进行数据的浅拷贝或者immutable
数据来达到更新。
2、当子组件props
中有函数时,可能会导致memo
失效。这是因为父组件重新渲染时,组件内的函数也会被重新创建,导致函数的prop
前后值不一致,子组件被动重新渲染。此时可以通过 useCallback
对函数进行缓存,避免子组件的非必要重渲染。
但是不要滥用useMemo
和useCallback
,只有在需要的时候才使用,否则可能反而会影响性能。
在 class
组件中,可以使用 React.PureComponent
进行类似的优化,原理也是差不多的。
这2节,我们讲到了如何避免 React 组件非必要的重新渲染,来提高性能。下一节,我们就通过 《React性能优化(四):提高页面渲染效率 🚀》 看看,当组件数据更新时,怎么提高页面渲染的效率。
转载自:https://juejin.cn/post/7170888515767500830