react 性能优化的三个API

react 相比于 Vue 的自由性导致了组件的渲染情况多变,如果出现了组件渲染变慢变重,这个时候就要考虑对 react 做性能优化。对 react 做性能优化之前很重要的一点就是,什么导致 react 变慢? 一个很重要的一点就是 react 频繁 render,所以本文的理解就是为了减少渲染次数。

react render

react 渲染本质都是由 state 变化引起的组件渲染,不论是 props 还是 context 本质其实都是 state 的变化引起的,props 传递的是父组件的 state,所以父组件 state 变化,那么使用父组件 state 的子组件就会重新渲染;context 的本质其实也是如此,所有的子组件消费来自祖先组件的 state,祖先组件的的 state 变化,消费这些 state 的组件就会 render。

memo 函数

react 渲染是由 state 变化导致的,那么有一个很常见的思路就是不使用父组件 state 或者说使用的父组件的 state 不变化的子组件不重复 render 不就可以解决这个问题了吗?react 就提供了一个 memo 函数,memo 函数缓存了 react 的上一次值,在父组件重新渲染的时候发现子组件被 memo 了,就不会重新渲染了。

当 child 组件没有缓存时,count 改变时 child 组件都会 render,即使 Child 组件并没有使用到父组件的 state,这种情况可以使用 Memo 函数缓存组件,这样下次 count 改变时,react 会对 memo 的组件进行检查 是否有影响它的 state 变化了,如果没有就会复用上一次的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function Father() {
const [count, setCount] = useState(1);
return (
<>
<button
onClick={() => {
setCount(count + 1);
}}
></button>
<Child></Child>
</>
);
}

function Child() {
console.log("render");
return <>Children</>;
}

改进

1
2
3
4
5
6
function Child() {
console.log("render");
return <>Children</>;
}

export default memo(Child);

useCallback

继续在上面的组件的基础上如果 child 是下面的例子,即使 memo 了也会不生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { memo } from "react";

export function Father() {
const [count, setCount] = useState(1);
const onChange = () => {
console.log("onChange");
};
return (
<>
<button
onClick={() => {
setCount(count + 1);
}}
></button>
<Child onChange={onChange}></Child>
</>
);
}

function Child({ onChange }) {
console.log("render");
return (
<>
children
<button onClick={onChange}></button>
</>
);
}

export default memo(Child);

这主要是因为 传递给 Child 组件的 onChange 函数在每次 Father 组件 re-render 的时候重新创建了,函数在内存中存放的是地址,地址不同了,那就是传递的 props 不同了,即使 Child 组件被 memo 了,仍热也会 re-render。这个时候就要使用缓存函数的 api useCallBack 了,被 useCallBack 缓存的函数 只要不是 usecallback 的依赖项改变,它缓存的函数就会是同一地址,这样的化就不会导致子组件的 re-render 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function Father() {
const [count, setCount] = useState(1);
const onChange = useCallback(() => {
console.log("onChange");
}, []);
return (
<>
<button
onClick={() => {
setCount(count + 1);
}}
></button>
<Child onChange={onChange}></Child>
</>
);
}

useMemo

useMemo 和 useCallback 很类似,都是起缓存作用的,只不过 useCallback 缓存函数,useMemo 缓存的是一个数值,文档推荐 useMemo 缓存耗时操作的数据;可以近似理解为 vue 中的 compute,当 useMemo 的依赖项发生变化时,重新计算,否则就复用上一次的值,不受 re-render 的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function Father() {
const [count, setCount] = useState(1);
const [number, setNumber] = useState(10000000);
const timeConsumingTask = useMemo(() => {
// …… 耗时任务
return timeConsumeResult;
}, count);

return (
<>
<button
onClick={() => {
setCount(count + 1);
}}
></button>
<Child onChange={onChange}></Child>
</>
);
}

何时使用呢

官方其实并不是很推荐过早的考虑做性能优化,因为上述提到的几个 API 缓存其实也会消耗一定的性能。正确的选择其实是开发时并不考虑型性能优化,当发现组件渲染的时候出现问题时
才进行上述的性能优化方案。
在平时的开发过程中,用得更多的手段其实是状态下沉

状态下沉

可以清晰的看到,除了对 ExpensiveTree 组件 memo 之外,更合适的其实是将 color 这个状态与 input 和 p 组件进行聚合 状态下沉下去

1
2
3
4
5
6
7
8
9
10
export default function App() {
const [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function App() {
return (
<>
<Form />
<ExpensiveTree />
</>
);
}

function Form() {
let [color, setColor] = useState("red");
return (
<>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
);
}

但是有一些使用场景应该注意做性能优化,当开发自定义 hook 或者说发布的组件时就要考虑使用这三个 hook 来进行优化
以我个人见解来说

  1. 状态下沉。
  2. 自定义 hook 自定义组件 使用 memo useCallback useMemo 提前性能优化
  3. 其他地方先不考虑性能优化,当组件出现 render 减慢的现象时,再使用这三个 API 进行优化

react 性能优化的三个API
https://sunburst89757.github.io/2022/10/28/performanceOptimise/
作者
Sunburst89757
发布于
2022年10月28日
许可协议