面试官:React 中如何做性能优化? 我:😰 ?😰 ? 😰 ?

2,823 阅读14分钟

虽然 React 提供了Virtual DOM/DOM Diff  等优秀的能力来提高渲染性能,但是在实际使用过程中,我们经常会遇到父组件更新,不需要更新子组件的场景,此时必须考虑利用 React 的渲染机制来进行优化。

类组件:shouldComponentUpdate

是什么

shouldComponentUpate() 通过返回布尔值来决定当前组件是否需要更新, 如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate()

作用及其使用方式

  • 使用场景: 类组件中,判断某个基础类型值被修改了才触发组件更新
  • 作用: 减少 rerender 次数

下面是一个只有当 props.color/state.count 的值改变,Child组件才重新渲染的例子,Parent组件修改自己的状态不会导致子组件渲染

/* eslint-disable max-classes-per-file */
import React from "react";

class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 1 };
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      // 重新渲染
      return true;
    }
    if (this.state.count !== nextState.count) {
      // 重新渲染
      return true;
    }
    // 不重新渲染
    return false;
  }

  render() {
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <div style={{ color: this.props.color }}>文字字字字字</div>
        <button
          color={this.props.color}
          onClick={() =>
            this.setState((state) => ({ count: state.count + 1 }))
          }>
          Count: {this.state.count}
        </button>
      </div>
    );
  }
}

function getRandomColor() {
  const colorAngle = Math.floor(Math.random() * 360);
  return `hsla(${colorAngle},100%,50%,1)`;
}

export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      childColor: "red",
      parentWord: "",
    };
    this.handleClick = this.handleClick.bind(this);
    this.handleClick2 = this.handleClick2.bind(this);
  }

  handleClick() {
    this.setState({
      childColor: getRandomColor(),
    });
  }

  handleClick2() {
    this.setState({ parentWord: `${(Math.random() * 100).toFixed(3)}` });
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>修改子组件的状态</button>
        <Child color={this.state.childColor} />
        <button onClick={this.handleClick2}>修改父组件自己的状态</button>
        <div>{this.state.parentWord}</div>
      </div>
    );
  }
}

注意事项

不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能

深比较也称原值相等,深比较是指检查两个对象的所有属性是否都相等,深比较需要以递归的方式遍历两个对象的所有属性, 操作比较耗时,深比较不管这两个对象是不是同一对象的引用

深层级比较且保持原 state/props 不可变的解决方案: immutable.js

immutable.js是 FaceBook 官方提出的不可变数据解决方案,主要解决了复杂数据在 deepClone 和对比过程中性能损耗

是什么东西呢?

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。 Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。 同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享), 即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

使用场景: 深层数据结构发生变化需要进行比较时考虑使用 immutable 对象加速嵌套数据的比较。

使用示例

yarn add immutable

下面是一个代码示例

/* eslint-disable max-classes-per-file */
import React from "react";
import Immutable from "immutable";

class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
  }

  shouldComponentUpdate(nextProps, nextState) {
    // 使用Immutable.is  直接比较对象的值
    if (!Immutable.is(this.props.childPrams, nextProps.childPrams)) {
      // 重新渲染
      return true;
    }
    // 不重新渲染
    return false;
  }

  render() {
    {
      /*
        尽管 props.childPrams 的值是相同的,但是每次传进来的 props.childPrams 引用地址不相同,因此会重新渲染
      */
    }
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <h3>{this.props.childPrams.getIn(["b", "c", "d", "e"])}</h3>
      </div>
    );
  }
}

const data = {
  a: "1",
  b: { c: { d: { e: "e" } } },
};

export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      childPrams: Immutable.fromJS(data),
      newchildPrams: Immutable.fromJS(data),
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    const { childPrams } = this.state;
    // updateIn 用来生成新的不可变数据
    const newchildPrams = childPrams.updateIn(
      ["b", "c", "d", "e"],
      (value) => `${value}1111`,
    );

    this.setState({
      newchildPrams,
    });
  }

  render() {
    return (
      <div>
        <h1>immutable使用示例</h1>
        <button onClick={this.handleClick}>修改子组件的状态</button>

        <Child childPrams={this.state.newchildPrams} />
        <div>{`this.state.childPrams: ${this.state.childPrams.getIn([
          "b",
          "c",
          "d",
          "e",
        ])}`}</div>
      </div>
    );
  }
}

上面的代码中,

  1. 使用Immutable.fromJS 针对深层此嵌套对象 生成不可被改变的数据,
  2. 使用 Immutable 的updateIn 来生成新的不可变数据(修改childPrams这个引用类型的值及其引用地址)
  3. 使用 Immutable 的getIn 来深层此嵌套对象的值
  4. 使用Immutable.is 直接比较对象的值
    1. Immutable.is 比较的是两个对象的 hashCodevalueOf(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。
  5. 结果: 只有 props.childPrams 的值改变了Child组件才会重新渲染

类组件: React.PureComponent

在大部分情况下,你应该继承 React.PureComponent 以代替手写 shouldComponentUpdate()

是什么

React.PureComponent以浅层对比 prop 和 state 的方式实现了 shouldComponentUpdate()

浅比较也称引用相等,在 javascript 中, == 是作浅比较,只检查左右两边是否是同一个对象的引用:

作用及其使用方式

  • 使用场景: 类组件中, prop 和 state 的浅比较
  • 作用: 减少 rerender 次数

下面是一个只有当 props.color/state.count 的值改变Child组件才会重新渲染的例子,Parent组件修改自己的状态不会导致子组件渲染

/* eslint-disable max-classes-per-file */
import React from "react";

class Child extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = { count: 1 };
  }

  render() {
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <div style={{ color: this.props.color }}>文字字字字字</div>
        <button
          color={this.props.color}
          onClick={() =>
            this.setState((state) => ({ count: state.count + 1 }))
          }>
          Count: {this.state.count}
        </button>
      </div>
    );
  }
}

export default class Parent extends React.Component {
  // 代码与shouldComponentUpdate的例子一样
  // .....
}

注意事项

  1. props/state 为复杂的引用类型时PureComponent无法处理(props 的值相等时子组件照样会继续渲染)

因为PureComponent只做了浅比较(只检查左右两边是否是同一个对象的引用)

尽管 props 的值是相同的,但是他们的引用地址不相同。 这样,每次传递给 props 的都是新的引用类型,导致每次浅比较结果都不相等

/* eslint-disable max-classes-per-file */
import React from "react";
// 深拷贝工具
const deepClone = (obj) => {
  let clone = obj;
  if (obj && typeof obj === "object") {
    clone = new obj.constructor();

    Object.getOwnPropertyNames(obj).forEach(
      (prop) => (clone[prop] = deepClone(obj[prop])),
    );
  }
  return clone;
};

class Child extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    {
      /*
        尽管 props.childPrams 的值是相同的,但是每次传进来的 props.childPrams 引用地址不相同,因此会重新渲染
      */
    }
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <h3>{this.props.childPrams.b.c.d.e}</h3>
      </div>
    );
  }
}

const data = {
  a: "1",
  b: { c: { d: { e: "e" } } },
};

export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      childPrams: data,
      newchildPrams: data,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 避免修改原来的状态,我们深拷贝生成一个新的对象
    const newchildPrams = deepClone(this.state.childPrams);

    newchildPrams.b.c.d.e = "eeeee";
    this.setState({
      newchildPrams,
    });
  }

  render() {
    return (
      <div>
        <h1>给子组件传递复杂的引用类型</h1>
        <h2>尽管 props.childPrams 的值是相同的,但是子组件还是会渲染</h2>

        <button onClick={this.handleClick}>修改子组件的状态</button>

        <Child childPrams={this.state.newchildPrams} />
        <div>
          {`this.state.childPrams.b.c.d.e:  ${this.state.childPrams.b.c.d.e}`}
        </div>
        <hr />
        <div>
          {`this.state.newchildPrams.b.c.d.e:  ${this.state.newchildPrams.b.c.d.e}`}
        </div>
      </div>
    );
  }
}
  1. 避免直接修改 state/props(保持数据的不可变特性)

下面是一个直接修改 state/props 导致 bug 的例子

/* eslint-disable max-classes-per-file */
import React from "react";

class Child extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    {
      /*
        尽管 props.childPrams 的值不同了,但是每次传进来的 props.childPrams 引用地址是相同的,因此不会渲染
      */
    }
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <h3>{this.props.childPrams.b.c.d.e}</h3>
      </div>
    );
  }
}

const data = {
  a: "1",
  b: { c: { d: { e: "e" } } },
};

export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      childPrams: data,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 修改原来的状态
    this.state.childPrams.b.c.d.e = "eeeee";
  }

  render() {
    return (
      <div>
        <h1>直接修改引用类型的 state/props导致的后果</h1>
        <h2>
          尽管 props.childPrams
          的值被修改了,但是引用地址还是相同的,导致子组件不会更新
        </h2>

        <button onClick={this.handleClick}>修改子组件的状态</button>

        <Child childPrams={this.state.childPrams} />
        <div>{`this.state.childPrams: ${this.state.childPrams.b.c.d.e}`}</div>
      </div>
    );
  }
}

保持数据不可变的解决方案: immutable.js

  1. 避免在 render 方法里创建函数

原因: 如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。

下面是一个在 render 方法里创建函数的例子

/* eslint-disable max-classes-per-file */
import React from "react";

class Child extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <h3 style={{ color: this.props.color }}>注意文字颜色</h3>
        {this.props.footer()}
      </div>
    );
  }
}

function getRandomColor() {
  const colorAngle = Math.floor(Math.random() * 360);
  return `hsla(${colorAngle},100%,50%,1)`;
}

export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      color: "red",
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({
      color: getRandomColor(),
    });
  }

  render() {
    return (
      <div>
        <h1>Render Props 与 React.PureComponent 一起使用时造成的BUG</h1>
        <button onClick={this.handleClick}>修改子组件的状态</button>
        {/*每次都会生成一个新的 footer函数*/}
        <Child color={this.state.color} footer={() => <h3>我来组成脚</h3>} />
      </div>
    );
  }
}

在这样例子中,每次 <Parent> 渲染,它会生成一个新的函数作为 <Child footer> 的 prop,因而在同时也抵消了继承自 React.PureComponent<Child> 组件的效果!

为了绕过这一问题,你可以定义一个 prop 作为实例方法

解决方案: 在 render 外部声明函数(当 render prop 不依赖外部状态可以这样做!)

/* eslint-disable max-classes-per-file */
import React from "react";

class Child extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    console.log("子组件渲染了");
    return (
      <div style={{ margin: 10, padding: 10 }}>
        <h3>注意文字颜色</h3>
        {this.props.footer()}
      </div>
    );
  }
}

function getRandomColor() {
  const colorAngle = Math.floor(Math.random() * 360);
  return `hsla(${colorAngle},100%,50%,1)`;
}

export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      color: "red",
    };
    this.handleClick = this.handleClick.bind(this);
    this.footer = this.footer.bind(this);
  }

  handleClick() {
    this.setState({
      color: getRandomColor(),
    });
  }

  // 定义为实例方法,`this.footer`始终
  // 当我们在渲染中使用它时,它指的是相同的函数
  footer() {
    console.log("====================================");
    console.log(this.state.color);
    console.log("====================================");
    return <h3 style={{ color: this.state.color }}>我来组成脚</h3>;
  }

  render() {
    return (
      <div>
        <h1>Render Props 与 React.PureComponent 一起使用时</h1>
        <h2>将render prop 作为实例方法产生新的问题</h2>
        <h2>
          如果render prop依赖了外部状态, 但是 render prop的 函数只绑定了一次,
          <br />
          导致 render prop不会更新
        </h2>
        <hr />
        <button onClick={this.handleClick}>修改子组件的状态</button>

        <Child footer={this.footer} />
      </div>
    );
  }
}

函数组件:React.memo

是什么

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现 这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

作用及其示例

  • 为什么使用:减少函数组件的渲染次数
  • 注意:
    • React.memo 只能检查 props 变更
    • React.memo 是一个高阶组件。

模拟 PureComponent

下面是一个只有当 props.color/state.count 的值改变Child组件才会重新渲染的例子,Parent组件修改自己的状态不会导致子组件渲染

import React, { useState } from "react";

const ChildOrigin = ({ color }) => {
  const [count, setCount] = useState(1);

  console.log("子组件渲染了");
  return (
    <div style={{ margin: 10, padding: 10 }}>
      <div style={{ color }}>文字字字字字</div>
      <button
        onClick={() => {
          setCount((prev) => prev + 1);
        }}>
        Count: {count}
      </button>
    </div>
  );
};
// 注意: React.memo作为高阶组件包裹原始组件生成一个新组件
const Child = React.memo(ChildOrigin);

function getRandomColor() {
  const colorAngle = Math.floor(Math.random() * 360);
  return `hsla(${colorAngle},100%,50%,1)`;
}

const Parent = () => {
  const [childColor, setChildColor] = useState("red");
  const [parentWord, setParentWord] = useState("");

  const handleClick = () => {
    const newcolor = getRandomColor();
    setChildColor(newcolor);
  };

  const handleClick2 = () => {
    const newparentWord = `${(Math.random() * 100).toFixed(3)}`;
    setParentWord(newparentWord);
  };

  return (
    <div>
      <h1>Reactmemo模拟PureComponent</h1>
      <h2>自动进行props的浅比较来决定是否更新</h2>
      <button onClick={handleClick}>修改子组件的状态</button>
      <Child color={childColor} />
      <button onClick={handleClick2}>修改父组件自己的状态</button>
      <div>{parentWord}</div>
    </div>
  );
};

export default Parent;

在上面的代码中,React.memo 实现了类似 PureComponent的效果,但它只比较 props(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)

模拟shouldComponentUpdate

注意: 与 shouldComponentUpdate 方法的返回值相反。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
  if (prevProps.color !== nextProps.color) {
    // 重新渲染
    return false;
  }
  if (prevProps.count !== nextState.count) {
    // 重新渲染
    return false;
  }
  // 不重新渲染
  return true;
}
export default React.memo(MyComponent, areEqual);

下面是一个只有当 props.color/state.count 的值改变Child组件才会重新渲染的例子,Parent组件修改自己的状态不会导致子组件渲染

import React, { useState } from "react";

const ChildOrigin = ({ color }) => {
  const [count, setCount] = useState(1);

  console.log("子组件渲染了");
  return (
    <div style={{ margin: 10, padding: 10 }}>
      <div style={{ color }}>文字字字字字</div>
      <button
        onClick={() => {
          setCount((prev) => prev + 1);
        }}>
        Count: {count}
      </button>
    </div>
  );
};

function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
  if (prevProps.color !== nextProps.color) {
    // 重新渲染
    return false;
  }
  // 不重新渲染
  return true;
}

const Child = React.memo(ChildOrigin, areEqual);

function getRandomColor() {
  const colorAngle = Math.floor(Math.random() * 360);
  return `hsla(${colorAngle},100%,50%,1)`;
}

const Parent = () => {
  const [childColor, setChildColor] = useState("red");
  const [parentWord, setParentWord] = useState("");

  const handleClick = () => {
    const newcolor = getRandomColor();
    setChildColor(newcolor);
  };

  const handleClick2 = () => {
    const newparentWord = `${(Math.random() * 100).toFixed(3)}`;
    setParentWord(newparentWord);
  };

  return (
    <div>
      <h1>Reactmemo模拟shouldComponentUpdate</h1>
      <h2>可以进行props的详细比较来决定是否更新</h2>
      <button onClick={handleClick}>修改子组件的状态</button>
      <Child color={childColor} />
      <button onClick={handleClick2}>修改父组件自己的状态</button>
      <div>{parentWord}</div>
    </div>
  );
};

export default Parent;

React.memo+ immutable 使用示例

下面是一个只有当 props的引用类型的值改变,Child组件才会重新渲染的例子,Parent组件修改自己的状态不会导致子组件渲染

import React, { useState } from "react";
import Immutable from "immutable";

const ChildOrigin = ({ childPrams }) => {
  console.log("子组件渲染了");
  return (
    <div style={{ margin: 10, padding: 10 }}>
      <h3>{childPrams.getIn(["b", "c", "d", "e"])}</h3>
    </div>
  );
};

function areEqual(prevProps, nextProps) {
  // 使用Immutable.is  直接比较对象的值
  if (!Immutable.is(prevProps.childPrams, nextProps.childPrams)) {
    return false;
  }
  // 不重新渲染
  return true;
}

const Child = React.memo(ChildOrigin, areEqual);

const data = Immutable.fromJS({
  a: "1",
  b: { c: { d: { e: "e" } } },
});

const Parent = () => {
  const [childPrams, setChildPrams] = useState(data);
  const [newchildPrams, setNewchildPrams] = useState(data);

  const handleClick = () => {
    const newchildPramsData = childPrams.updateIn(
      ["b", "c", "d", "e"],
      (value) => `${value}${(Math.random() * 100).toFixed(3)}`,
    );
    setNewchildPrams(newchildPramsData);
  };

  return (
    <div>
      <h1>React.memo+ immutable 使用示例</h1>
      <button onClick={handleClick}>修改子组件的状态</button>

      <Child childPrams={newchildPrams} />
      <div>{`childPrams: ${childPrams.getIn(["b", "c", "d", "e"])}`}</div>
    </div>
  );
};

export default Parent;

上面的代码中,

  1. 使用Immutable.fromJS 针对深层嵌套对象生成不可被改变的数据
  2. 使用 Immutable 的updateIn 来生成新的不可变数据(修改childPrams这个引用类型的值及其引用地址)
  3. 使用 Immutable 的getIn 获取深层嵌套对象的值
  4. 使用Immutable.is 直接比较对象的值
    1. Immutable.is 比较的是两个对象的 hashCodevalueOf(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。
  5. 结果: 只有 props.childPrams 的值改变了Child组件才会重新渲染

函数组件:React.useMemo

是什么

把“创建”函数(useMemo的第一个参数)和依赖项数组(useMemo的第二个参数)作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算。这种优化有助于避免在每次渲染时都进行高开销的计算。

使用场景:

  • 某个依赖项的值是否已被改变
  • 存在大量昂贵计算的场景

作用: 返回一个被缓存的值

注意: React.useMemo 只进行浅比较

场景一:当某个依赖项被改变才更新子组件

function Parent({ a, b }) {
  // 只有a改变了 Child1才会重新渲染:
  const child1 = React.useMemo(() => <Child1 a={a} />, [a]);
  // 只有b改变了 Child2才会重新渲染:
  const child2 = React.useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  );
}

React.memo有异曲同工之妙

function MyComponent(props) {
  /* 使用 props 渲染 */
}

export default React.memo(MyComponent);

场景二:缓存大量昂贵(耗时/耗内存)计算的值

假设你有一个函数需要同步计算一个值,该值在计算上是非常昂贵的,则可以使用React.useMemo

function RenderPrimes({ iterations, multiplier }) {
  const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier,
  ]);
  return <div>Primes! {primes}</div>;
}

上面 👆 的代码中,不管RenderPrimes组件重新渲染多少次, 只有当iterations/multiplier改变时, 才会重新计算primes

下面 👇 是一个计算更复杂的 🌰

const List = React.useMemo(
  () =>
    listOfItems.map((item) => ({
      ...item,
      // 昂贵的计算函数1
      itemProp1: expensiveFunction(props.first),
      // 昂贵的计算函数2
      itemProp2: anotherPriceyFunction(props.second),
    })),
  [listOfItems],
);

有一个叫memoize-one库也可以实现缓存计算结果的作用

问题:当 props 为 function 时 子组件还是会重新渲染

import React, { useState } from "react";

const Child = React.memo(({ header, footer }) => {
  console.log("子组件渲染了");
  return (
    <div style={{ margin: 10, padding: 10 }}>
      {header()}
      <h3>body</h3>
      {footer()}
    </div>
  );
});

const Parent = () => {
  const [count, setCount] = useState(1);

  const header = () => <h2>我来组成头部</h2>;

  const footer = () => <h2>我来组成腿部</h2>;

  return (
    <div>
      <h1>React.memo使用示例</h1>
      <h2>当props为function时 子组件还是会重新渲染</h2>
      <button
        onClick={() => {
          setCount((prev) => prev + 1);
        }}>
        修改父组件自己的状态:{count}
      </button>
      <Child header={header} footer={footer} />
    </div>
  );
};

export default Parent;

我们后面使用 useCallback 来解决


如果在应用程序中过于频繁地使用useMemo,则可能会损害性能。

我们应该先使用性能分析工具进行检查,以识别昂贵的性能问题--昂贵意味着它正在消耗大量资源(如内存)。

比如: 在render函数中定义了大量变量,那么使用useMemo进行缓存是有意义的。

函数组件:React.useCallback

是什么

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本, 该回调函数仅在某个依赖项改变时才会更新。

当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

  • 作用:返回一个被缓存的函数

当用useCallback包装时,React 保存对函数的引用。 将此引用作为属性传递给新组件,以减少渲染时间。

场景:避免在 render 中创建函数(不必每次渲染都重新生成了)

解决 render prop 为函数时子组件重复渲染的问题

如果不使用 React.useCallback包裹, 直接使用 header/footer ,会导致子组件重复渲染

import React, { useState } from "react";

const Child = React.memo(({ header, footer }) => {
  console.log("子组件渲染了");
  return (
    <div style={{ margin: 10, padding: 10 }}>
      {header()}
      <h3>body</h3>
      {footer()}
    </div>
  );
});

const Parent = () => {
  const [count, setCount] = useState(1);

  const header = () => <h2>我来组成头部</h2>;
  // 如果不使用 React.useCallback包裹, 直接使用 header ,会导致子组件重复渲染
  const newHeader = React.useCallback(header, []);

  const footer = () => <h2>我来组成腿部</h2>;
  const newFooter = React.useCallback(footer, []);

  return (
    <div>
      <h1>React.memo+ React.useCallback</h1>
      <h2>使用React.useCallback包裹render prop不会导致重复渲染</h2>
      <button
        onClick={() => {
          setCount((prev) => prev + 1);
        }}>
        修改父组件自己的状态:{count}
      </button>
      <Child header={newHeader} footer={newFooter} />
    </div>
  );
};

export default Parent;

数组元素遍历加上 key

作用: 提高 diff 算法的 dom 重复利用率

function List(props) {
  const { data } = props;
  return (
    <ul>
      {data.map((item, index) => {
        return <li key={item.id || index}>{item.name}</li>;
      })}
    </ul>
  );
}

 ReactDOM.unstable_batchedUpdates 实现批量状态更新

尽量使用<>...</> 或者<React.Fragment> 来包裹元素

使用<>...</> 或者<React.Fragment>不会生成多余标签

作用:避免无用标签嵌套

使用 React.lazy 和 Suspense

是什么

Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。

目前,Suspense 仅支持的使用场景是:通过 React.lazy 动态加载组件。它将在未来支持其它使用场景,如数据获取等。

代码将会在首次渲染时,才自动导入并进行加载。

  1. React.lazy 接受一个函数 A
  2. 函数 A 需要调用 import()返回一个 Promise
  3. 返回的 Promise 需要 resolve 一个 defalut export 的 React 组件。

React.lazy 加载的组件只能在 <Suspense>组件下使用

<Suspense>使在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)

作用及示例

作用: 减少资源的加载

代码如下:

import React, { Suspense, Component } from "react";

const LazyComponent = React.lazy(() => import("./components/LazyComponent"));

class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {
      show: false,
    };
    this.handleClick = this.handleClick.bind(this);
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>按钮</button>
        {this.state.show && (
          <Suspense fallback={<div>Loading...</div>}>
            <LazyComponent />
          </Suspense>
        )}
      </div>
    );
  }
  handleClick() {
    this.setState({
      show: true,
    });
  }
}

export default Home;

结果如下图所示,我们可以看到点击按钮后 2.chunk.js 才加载出来,页面上先会显示 'Loding...', 然后再显示异步组件的内容。

v2-0fa7814d25721797254917432ac5d368_b

使用 React.lazy@loadable/components 的区别:

  • React.lazy 是由 React 官方维护,推荐的代码拆分的解决方案。
  • React.lazy 只能与 Suspense 一起使用,而且不支持服务端渲染。
  • @loadable/component 支持服务端渲染。

参考 @loadable/components 与 React.lazy 的官方对比

服务端渲染(SSR)

什么是服务端渲染

将渲染的工作放在服务端进行。

这种方式很早就存在,早在 Ajax 出现之前全部都是这种方式, 由服务端返回给浏览器完整的 html 内容。

浏览器得到完整的结构后就可直接进行 DOM 的解析、构建、加载资源及后续的渲染

服务端渲染的优缺点

这种页面(html)直出的方式可以让页面首屏较快的展现给用户,对搜索引擎比较友好,爬虫可以方便的找到页面的内容,非常有利于 SEO

不好的地方就是所有页面的加载都需要向服务器请求完整的页面内容和资源,访问量较大的时候会对服务器造成一定的压力,另外页面之间频繁刷新跳转的体验并不是很友好

如何使用服务端渲染

总结

本文探讨了在 React 应用程序中如何通过代码实现性能优化,及其常见的坑。下面我们总结一下

  • 可以结合 React 的shouldComponentUpdate PureComponent 以及 React.memo 等做浅比较处理;
  • 涉及到不可变数据的处理和深比较时可以使用immutable.js;
  • 当然也可以结合使用 useMemo/useCallback 实现计算的值或者函数的缓存
  • 如果为了优化首屏渲染效率和 SEO,可以使用服务端渲染

注意: 过早/过度优化是万恶之源

如果在应用程序中过于频繁地使用useMemo/useCallback等,则可能会损害性能--使用 Hooks 的次数越多,应用程序必须分配的内存就越多

我们应该先使用性能分析工具进行检查,以识别昂贵的性能问题(代码哪一步在耗时耗内存),然后针对性的进行优化

最后

你还知道哪些 React 性能优化方式呢?欢迎在评论区留下的你的见解!文章浅陋,欢迎各位看官不吝赐教

觉得有收获的朋友欢迎点赞关注一波!

like-me

往期文章

  1. 企业级前端开发规范如何搭建 🛠
  2. 企业级 CI 工具链如何搭建
  3. React 声明组件的几种方式及注意事项
  4. 响应式设计的前置知识理解