前言
为什么要写了解性能优化?很简单,因为网页加载慢!
从用户角度来说,使用页面最重要最直观的是让用户觉得网页加载快、操作流畅,React虽然已经很高效了,但是组件的频繁重渲染会导致页面性能的急剧下降。因此需要了解React的渲染机制,从而提升用户体验。
但是性能优化也不能随便优化,盲目地添加性能优化的hooks可能反导致性能的下降,因此在实践之前需要先简单了解React的核心机制,帮助自己快速准确地实现优化。
此外,性能优化的理解实质上也是对React思维的理解。 React的核心思想是根据状态渲染组件,状态变化了组件就需要刷新。 但是实际上不是所有的状态变化都需要重新渲染所有组件,因此理解性能优化的逻辑,也就是在理解React的刷新机制、虚拟DOM的diff逻辑和函数组件的闭包和引用特性。
一、React.memo
React.memo是React的高阶组件,用于缓存函数组件的渲染结果,只有当props发生变化时才会重新渲染,换句话说,memo就是用于给函数组件加上浅层的props比较,防止无意义的重复渲染。
给一个举例组件:
import React, { memo } from 'react';
interface UserCardProps {
name: string;
age: number;
}
const UserCard = memo(({ name, age }: UserCardProps) => {
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
</div>
);
});
export default UserCard;
/** 使用举例 */
<UserCard name="Tom" age={18} />
如果父组件重新渲染,但传入子组件的props没有变化,则子组件不会重新渲染。
React默认的渲染逻辑是父组件渲染,则子组件都会重新执行render,但是有些子组件的重渲染是没必要的,这个时候就需要引入memo。memo是在渲染的基础上加一个浅比较的锁,如果前后的props一致,就不需要渲染该子组件。
但是这不等于所有的组件都要加上memo,因为比较props也是需要成本的,只有当比较成本<渲染成本时,才推荐使用memo。
此外,函数和对象是一种很特殊的存在,如果组件中的props是函数或者对象,那么函数或对象因为每次执行时都会导致引用的变化,必然导致浅比较变化,从而导致memo变化。
因此需要引入合理的钩子。
二、useCallback
函数不搭配useCallback时,每次执行函数,都会创建一个新的函数引用,memo只做浅比较,认为引用变了则必然导致props变化,从而使子组件重新渲染。包装一层useCallback会让函数在依赖不变时返回同一个函数引用。
import React, { useState, useCallback } from 'react';
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('子组件变化');
return <button onClick={onClick}>Click Me</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// 使用 useCallback,函数引用不变
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
console.log('父组件变化');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
<Child onClick={handleClick} />
</div>
);
};
现在点击Add时:
- 父组件会渲染,因为状态变化了(count是用state控制的)
- 子组件不会变化,因为onClick函数引用没有变
补充:什么是依赖
放在这里做个补充,了解一下函数和对象依赖。
在React中,useCallback和useMemo都有第二个参数:一个依赖数组。
例如:
const fn = useCallback(() => {
console.log(count);
}, [count]);
const data = useMemo(() => ({ value: count * 2 }), [count]);
这里的[count]就是依赖数组,意思是只有count发生变化时,React才会重新创造这个函数,当count没变时,React会复用上一次的函数引用。
为什么我们要关注依赖?因为依赖会告诉React这个函数/对象什么时候应该重新生成。
例如:
const handleClick = useCallback(() => {
console.log(count);
}, []);
此处的没有把count作为函数依赖,但是放了依赖数组,则执行函数时,函数引用没有变化,但是执行该函数的count永远都是第一次的值,因为压根没有依赖。所以依赖数组的判定依据是:里面的每个值,是“在函数或计算中用到的外部变量”。
三、useMemo
useMemo用于缓存对象引用。
const Child = React.memo(({ user }: { user: { name: string } }) => {
console.log('子组件变化');
return <div>{user.name}</div>;
});
const Parent = () => {
const [count, setCount] = useState(0);
const user = { name: 'Jseeza' }; // 每次 render 都是新对象
return (
<div>
<button onClick={() => setCount(count + 1)}>Add</button>
<Child user={user} />
</div>
);
};
上述代码没有使用useMemo缓存对象,则将对象传给子组件传时,即便对象没有发生变化,但每次都会生成一个新的引用,从而导致子组件反复重新渲染。
我们再看看添加了useMemo的效果:
import React, { useState, useMemo } from 'react';
const Child = React.memo(({ user }: { user: { name: string } }) => {
console.log('子组件变化');
return <div>{user.name}</div>;
});
const Parent = () => {
const [count, setCount] = useState(0);
const user = useMemo(() => ({ name: 'Jseeza' }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Add</button>
<Child user={user} />
</div>
);
};
现在点击Add时,parent渲染不变,则child不会重复渲染。
四、useEffect
理论上来说不应该把useEffect放在这里,但是有人会把这个概念混淆在一起(是的是我)。
useEffect不会直接搭配memo,但两者会在逻辑上间接配合使用。 需要理清两者定位:
- memo:让组件在props不变时跳过渲染,关注点在于是否渲染
- useEffect:在组件渲染后执行副作用逻辑(请求、监听、DOM操作等),关注点在于渲染后做什么事
const Child = React.memo(({ userId }: { userId: number }) => {
useEffect(() => {
console.log('fetch user:', userId);
// 模拟发请求
}, [userId]);
return <div>User ID: {userId}</div>;
});
const Parent = () => {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(1);
console.log('父组件重新渲染');
return (
<div>
<button onClick={() => setCount(count + 1)}>Count +1</button>
<button onClick={() => setUserId(userId + 1)}>User +1</button>
<Child userId={userId} />
</div>
);
};
上述过程中:
- 点击“Count+1”时:
- 父组件重新渲染
- props中的userId没有变化
- memo浅比较发现props无变化,不会重新渲染子组件
- useEffect不执行
- 点击“User+1”时:
- 父组件重新渲染
- props的userId变化
- memo浅比较发现props变化,重新渲染
- useEffect依赖userId,执行副作用逻辑
五、总结
| 目标 | 推荐方案 |
|---|---|
| 避免子组件不必要的重复渲染 | 用memo包裹组件 |
| 控制副作用在特定数据变化时渲染 | 用useEffect的依赖数组 |
| 避免父组件传递的函数/对象导致子组件重复渲染 | 用useCallback/useMemo配合memo |