React渲染优化

React Component渲染优化

react渲染遵循一个基本原则:状态的改变导致重刷新, 然而具体到实际的应用中,一般可以认为一下规则导致了re-render:

  1. state变化导致了re-render,state变化会导致state所属组件以及所有子组件re-render
  2. 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的解决方案

  1. 缩小范围,将状态尽量下放到组件内,通过将组件状态尽可能低的下放到子组件内,缩小了re-render范围
  2. 利用props特性将component作为props传入组件内,强制不刷新,但是需要根据业务判断组件是否会一直re-render,否则可能优化得不偿失
  3. 使用memo包裹组件,避免不必要刷新,当组件props不常变化并且re-render cost很高的时候,这个时候可以考虑使用memo包裹组件以