React Component渲染优化
react渲染遵循一个基本原则:状态的改变导致重刷新, 然而具体到实际的应用中,一般可以认为一下规则导致了re-render:
- state变化导致了re-render,state变化会导致state所属组件以及所有子组件re-render
- context变化导致了re-render,context变化会导致所有context的消费者以及消费者的所有子组件re-render,从这个行为上讲,可以认为context是一个全局的、可以被任意组件使用的state
根据以上的规则,可以获知当一个组件的父组件渲染时,无论子组件是否有状态变化,都会导致re-render,这也是react渲染优化的重点,即尽可能的刷新那些需要刷新的组件。
1. 缩小刷新范围,将状态尽可能下放
业务层面上可能会遇到一种组件,只需要显示一个按钮或类似的小型区域,并且有一定的交互功能,这个时候可以把所有的状态尽量下放到组件内部,而不是放在父组件里,比如我们常见的使用dialog显示图片的需求: 优化前:
export default function Demo1() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(true)}></button>
{visible && (
<Modal open={visible}>
<div>pic preview here</div>
</Modal>
)}
<div>some other components here</div>
</div>
);
}
优化后:
function PicPreview() {
const [visible, setVisible] = useState(false);
return (
<>
<button onClick={() => setVisible(true)}></button>
{visible && (
<Modal open={visible}>
<div>pic preview here</div>
</Modal>
)}
</>
);
}
export default function Demo1() {
return (
<div>
<PicPreview />
<div>some other components here</div>
</div>
);
}
通过将state下放到picpreview组件里,使得visible的变化无法影响到demo1的渲染,所有的re-render都发生在picpreview内部。 除此之外,当medal这类通过条件显示的元素,在不显示的时候需要从dom上清除元素
{visible && (
<Modal open={visible}>
<div>pic preview here</div>
</Modal>
)}
// 不应该使用下面形式
<Modal open={visible}>
<div>pic preview here</div>
</Modal>
所以需要通过visible来控制modal在dom上的存在与否,虽然这样会使得组件库(比如antd)失去onClose回调功能 一般的组件库都会在medal关闭时删除dom元素,或者比如antd也提供了destroyOnClose这样的属性,让用户决定是否应该保留元素,但是即使使用了这样的属性,在进入demo1组件时,dom没有medal元素,但react仍会执行medal里的函数,造成不必要的开销:
function PicPreview() {
const [visible, setVisible] = useState(false);
const a = [1, 2, 3];
return (
<>
<Button onClick={() => setVisible(true)}>show</Button>
// {visible && (
<Modal
open={visible}
destroyOnClose={true}
onCancel={() => setVisible(false)}
>
<div>pic preview here</div>
{a.map((item) => {
console.log("medal 运行了");
//可以通过console看到medal运行了,即使dom上没有这个元素
return <div key={item}>{item}</div>;
})}
</Modal>
// )}
</>
);
}
2. 将component作为props
由于react单项数据流(one-way data flow)的特性,子组件并不能更改外部传入的props,那么我们可以通过props传入component,实现强制不渲染子组件的效果 优化前
function C1() {
return <div>c1</div>;
}
function C2() {
return <div>c2</div>;
}
export default function Demo1() {
return (
<>
<div>demo1</div>
<C1 />
<C2 />
</>
);
}
优化后
function C1() {
return <div>c1</div>;
}
function C2() {
return <div>c2</div>;
}
type Props = {
C1: ReactNode;
C2: ReactNode;
};
function Demo1({ C1, C2 }: Props) {
return (
<>
<div>demo1</div>
{C1}
{C2}
</>
);
}
export default function Demo2() {
return <Demo1 C1={C1()} C2={C2()} />;
}
当demo1里有state并触发更新时,c1与c2都不会re-render。但是如果C1状态有更新,那么由于props的特性,c1 props发生了变化,将会导致c2 以及demo也会re-render,所以这种方式可以采用,但是需要结合业务实际场景。如果c1c2需要频繁变化,使用优化前的方式反倒可能会减少re-render范围
3. 使用memo包裹组件
首先react官方文档 (opens in a new tab)明确指出除非1. 你的组件render cost很高,并且渲染的props总是相同的,那么这个时候可以考虑使用memo,否则基本不需要memo。 memo可以把一个组件memoized类似于useMemo的作用 按照上文所述,state变化会导致re-render,并且将所有子组件一并re-render,但是某些情况下子组件的props并没有变,所我们可能会需要memo这样的功能去阻止re-render。 eg.
import { Button } from "antd";
import { memo, useState } from "react";
function C1() {
console.log("c1");
const [count, setCount] = useState(0);
return (
<div>
c1 <div>c1:{count}</div>
<Button onClick={() => setCount(count + 1)}>add c1 count</Button>
</div>
);
}
function C2() {
console.log("c2");
const [count, setCount] = useState(0);
return (
<div>
c2 <div>c2:{count}</div>
<Button onClick={() => setCount(count + 1)}>add c2 count</Button>
</div>
);
}
const MemoizedC1 = memo(C1);
export default function Demo3() {
const [count, setCount] = useState(0);
return (
<>
<div>
demo3<div>demo3:{count}</div>
<Button onClick={() => setCount(count + 1)}>add demo3 count</Button>
</div>
<div>
<MemoizedC1 />
</div>
<div>
<C2 />
</div>
</>
);
}
运行以上的例子可以发现当demo3的状态变化时,c2会随之re-render,但是c1不会 但如果c1的props每次都变化,那么memo作用就被完全消除了:
import { memo, useState } from "react";
type C1Props = {
number: number;
};
function C1({ number }: C1Props) {
console.log("c1");
const [count, setCount] = useState(0);
return (
<div>
c1 <div>c1:{count}</div>
<button onClick={() => setCount(count + 1)}>add c1 count</button>
<div>c1 props number : {number}</div>
</div>
);
}
function C2() {
console.log("c2");
const [count, setCount] = useState(0);
return (
<div>
c2 <div>c2:{count}</div>
<button onClick={() => setCount(count + 1)}>add c2 count</button>
</div>
);
}
const MemoizedC1 = memo(C1);
export default function Demo3() {
const [count, setCount] = useState(0);
const [c1Number, setC1Number] = useState(0);
return (
<>
<div>
demo3<div>demo3:{count}</div>
<button onClick={() => setCount(count + 1)}>add demo3 count</button>
<button onClick={() => setC1Number(c1Number + 1)}>
add c1number count
</button>
</div>
<div>
<MemoizedC1 number={c1Number} />
</div>
<div>
<C2 />
</div>
</>
);
}
以上代码当在demo3中增加number时,c1仍旧会每次re-render,所以当props频繁改变时,memo优化没有作用 如果想避免不必要的变化,也可以将props使用usememo包裹起来:
import { memo, useMemo, useState } from "react";
type C1Props = {
number: { number: number };
};
function C1({ number }: C1Props) {
console.log("c1");
const [count, setCount] = useState(0);
return (
<div>
c1 <div>c1:{count}</div>
<button onClick={() => setCount(count + 1)}>add c1 count</button>
<div>c1 props number : {number.number}</div>
</div>
);
}
function C2() {
console.log("c2");
const [count, setCount] = useState(0);
return (
<div>
c2 <div>c2:{count}</div>
<button onClick={() => setCount(count + 1)}>add c2 count</button>
</div>
);
}
const MemoizedC1 = memo(C1);
export default function Demo3() {
const [count, setCount] = useState(0);
const [c1Number, setC1Number] = useState({ number: 0 });
const useMemoNumber = useMemo(() => c1Number, [c1Number]);
return (
<>
<div>
demo3<div>demo3:{count}</div>
<button onClick={() => setCount(count + 1)}>add demo3 count</button>
<button onClick={() => setC1Number({ number: c1Number.number + 1 })}>
add c1number count
</button>
</div>
<div>
<MemoizedC1 number={useMemoNumber} />
</div>
<div>
<C2 />
</div>
</>
);
}
总结
本文简单说明了react的刷新触发机制,并且给出了三种能够一定程度上减少re-render cost的解决方案
- 缩小范围,将状态尽量下放到组件内,通过将组件状态尽可能低的下放到子组件内,缩小了re-render范围
- 利用props特性将component作为props传入组件内,强制不刷新,但是需要根据业务判断组件是否会一直re-render,否则可能优化得不偿失
- 使用memo包裹组件,避免不必要刷新,当组件props不常变化并且re-render cost很高的时候,这个时候可以考虑使用memo包裹组件以