文章正文

React Profiler 的使用

2021-09-14 发布于 · 阅读量:204

React Profiler 的使用

前言

平时大家开发项目的时候,有时候会感觉项目卡顿,通常情况下可以即时做出整改,但也有时候不太容易找到引起卡顿的点,或者说不好发现潜在的性能问题,React Developer Tools 提供的 Profiler 可以直观的帮助大家找出 React 项目中的性能瓶颈,进一步来改善我们的应用,推荐给大家安装使用。

  • 从概念上讲,React 分为两个阶段工作,React 的生命周期图谱如下所示:

    • 渲染阶段 会确定需要进行哪些更改,比如 DOM 。在此阶段 React 调用 render,然后将结果与上次渲染的结果进行比较。

    • 提交阶段 发生在 React 应用变化时。在此阶段 React 还会调用 componentDidMountcomponentDidUpdate 之类的生命周期方法。( 对于 React DOM 来说,会发生在 React 插入,更新及删除 DOM 节点的时候。)

      Profiler 是在提交阶段收集性能数据的,所以不能定位非提交阶段的性能问题。

使用

  • 安装

  • 介绍

    • 下图为面板按钮基本功能

    • 打开设置可以记录组件 rendered 的原因

    • 还可以高亮发生 render 的组件

  • 演示

    • 为了方便大家阅读展示面板的信息,我们以最简单的例子来演示:

      import React from "react";
      const style = {
        display: "flex",
        justifyContent: "space-around",
        maxWidth: 800,
        margin: "0 auto",
        padding: 60,
      };
      const Display = (props) => {
        console.log("Display");
        return <pre>{JSON.stringify(props.data, null, 2)}</pre>;
      };
      const Count = (props) => {
        console.log("count");
        return <p>{props.data}</p>;
      };
      // Anonymous
      export default class extends React.Component {
        state = {
          count: 0,
        };
        handleAdd = () => {
          this.setState({
            count: this.state.count + 1,
          });
        };
        onChange = (key) => (e) => {
          this.setState({
            [key]: e.target.value,
          });
        };
        render() {
          const { text, password, count } = this.state;
          return (
            <div>
              <div style={style}>
                <div>
                  <input type="text" value={text || ""} onChange={this.onChange("text")} />
                  <br />
                  <br />
                  <input type="text" value={password || ""} onChange={this.onChange("password")} />
                </div>
                <Display data={{ text, password }} />
              </div>
              <div align="center">
                <Count data={count} />
                <button onClick={this.handleAdd}>add</button>
              </div>
            </div>
          );
        }
      }
    • 按如下步骤操作:

      1. 点击 reload 按钮,等待页面加载完成;
      2. 在 input 输入内容,使页面发生 render
      3. 点击 add button ,再次使页面 render
      4. 停止。

    • 然后 Profiler 生成如下的信息:

      1. A 区对应了本次 record 期间的 提交 次数,每一列都表示一次提交的数据。

        • 列的颜色和高度对应该次提交渲染所需的时间 (较高的黄色比较短的绿色耗费时间长);
        • 我们可以忽略掉最短的灰色列,灰色代表没有重新渲染;
      2. A 区较高的 6 列则对应了我们上面的步骤操作:

        • 第一列对应页面的 mount ,因为是首次渲染,所以最高,代表耗时最长;
        • 第二、三列对应了 input 输入文字引发的两次渲染;
        • 最后三列则对应了 add button 三次点击引发的渲染。
      3. 左右切换 A 区的数据,表示了选中列的提交信息就会展示在 B 区,同时在 C 区展示应用程序内组件(如 Display 、Count )的详细信息。

        • Committed at 表示相对于本次 record 的时间,可以忽略;
        • Render duration 表示本次提交渲染耗时,我们需要关注这个;
      4. 例如 06/11 这次提交,整个 Anonymous 组件用了 1ms 来渲染, 但本身只耗费了 0.2ms,即图中的 0.2ms of 1ms,剩余的 0.8ms 用在其子级的渲染上。 子组件 DisplayCount 也有自己对应的渲染时间,以此类推。

        • 组件的宽度及颜色表示渲染所耗费的时间,同样是黄色时间较长;
      5. 为了更方便的查看组件的耗时,我们可以切换 Ranked 排序图,可以很清楚的看到耗费时间最长的那个组件。

      6. 例如 10/11 这次提交,操作上只是点击了 add button 来更新 Count, 但是这里 Display 却是最耗时的那个。

        • 单击选中 Display,可以在右侧看到 6 次rendered 信息, 上方的 Why did this render? 记录了每次 rendered 的原因;
      7. 如果你非常了解这里的代码,可以非常容易想到下一步就是优化 Display 的代码,因为这里的 props.data 看起来并没有发生什么变化。当然也可以在这个时候切换到 Components 选项卡,来确认你的想法,这里有组件更为详细的信息。

        • <> 可以查看源码;
        • 🐞 可以在控制台打印组件信息;
    • 阻止重新渲染

      改变 DisplayCount 的写法,保证两个组件 reRender 只是因为自身属性发生了变化,我们再来看一下效果。

      const Display = React.memo(
        (props) => {
          console.log("Display");
          return <pre>{JSON.stringify(props.data, null, 2)}</pre>;
        },
        (prev, next) => {
          return JSON.stringify(prev) === JSON.stringify(next);
        }
      );
      const Count = React.memo((props) => {
        console.log("count");
        return <p>{props.data}</p>;
      });

      再重复执行一次上面的操作,看一下结果。

      很遗憾,虽然 DisplayReact.memo 的比较函数之下,已经不再重新 render。但是 Display 的渲染时间和应用的渲染时间相比改写之前都变大了,这说明 memo 函数的比较时间大于组件自身的渲染时间,在当前这个简单的应用程序下,以 React.memo 来 "优化" 应用是得不偿失的。

  • 改进

    现在我们知道如何阅读 Profiler 的展示面板以及生成的图表信息,为了更直观的感受到阻止 reRender的效果,我们在例子中增加一个常见的 List 再来看一下。

    import { List, Avatar } from "antd";
    const Length100List = ({ data }) => {
      return (
        <List
          itemLayout="horizontal"
          dataSource={data}
          renderItem={(item) => (
            <List.Item key={item.id}>
              <List.Item.Meta
                avatar={<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />}
                title={item.name.last}
                description={item.email}
              />
              <div>{item.nat}</div>
            </List.Item>
          )}
        />
      );
    };
    // list 代表一个长度为100的数组,取自 https://randomuser.me/api/?results=100&inc=name,gender,email,nat&noinfo
    <div style={style2}>
      <Length100List data={list} />
    </div>;

    我们点击 add button 两次,使页面 render, 然后可以看到 Profiler 记录的信息如下:

    很明显,未加优化的 Length100List 占用了大部分 commit 时间,而这个时间很明显是不必要的,我们使用 React.memo 来阻止 List 的不必要渲染。

    const PureListItem = React.memo(({ item }) => {
      return (
        <List.Item key={item.id}>
          <List.Item.Meta
            avatar={<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />}
            title={item.name.last}
            description={item.email}
          />
          <div>{item.nat}</div>
        </List.Item>
      );
    });
    const Length100List = React.memo(({ data }) => {
      return <List itemLayout="horizontal" dataSource={data} renderItem={(item) => <PureListItem item={item} />} />;
    });

    再看一下效果:

    现在 commit 时间最长的就是我们点击add button 更新数据的地方。嗯,满意!

优化方式

  • shouldComponentUpdate()

针对不同的业务场景,这里的比较函数会有不同的写法,比如仅仅比较 props 的某个属性,或与本文中的例子一样以 JSON.stringify 来直接比较 props。对于复杂的数据结构,如果需要阻止 reRender,不建议进行深层比较或者使用 JSON.stringify,这样非常影响效率。可以考虑使用 immutable 来加速嵌套数据的比较,关于 immutable 的使用,可以查看 15 分钟学会 Immutable。你可以去实现自己的 CustomComponent,以达到和 PureComponent 一样的使用方式和目的。

  • 后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且当返回 false 时,仍可能导致组件重新渲染 (意思就是 hook 大法好)
  • 如今由于函数组件和 hook 的使用,这样的优化场景已经大大减少了;
import React from "react";
import { is } from "immutable";
export default class extends React.Component {
  shouldComponentUpdate(nextProps = {}, nextState = {}) {
    if (
      Object.keys(this.props).length !== Object.keys(nextProps).length ||
      Object.keys(this.state).length !== Object.keys(nextState).length
    ) {
      return true;
    }
    for (const key in nextProps) {
      if (!is(this.props[key], nextProps[key])) {
        return true;
      }
    }
    for (const key in nextState) {
      if (!is(this.state[key], nextState[key])) {
        return true;
      }
    }
    return false;
  }
}
  • React.PureComponent

    React.PureComponent 依靠 shouldComponentUpdate 实现了一层 shallowEqual,仅作对象的浅层比较,以减少跳过更新的可能性,但是如果对象中包含复杂的数据结构,则有可能产生错误的比对,所以 PureComponent 会更多的运用于较为简单的 props & state 展示组件上。

    React.memo 与其原理一样,只是用于 函数组件 上,回调函数的返回值与 shouldComponentUpdate 相反;

  • Hook

    React 提供的诸如 useEffectuseMemouseCallback 等钩子函数,他们都带有 memoized 属性,他们的第二个参数都是一个值数组,当值数组的数据发生变化时,hook函数会重新执行。虽然 hook 解决了一些类组件的痛点,但是 hook 的依赖项对比依然存在着上述痛点,并且这里的依赖项有时候会很长,社区里依然有让官方添加自定义比较功能的需求,不过官方给出的 自定义hook 已经可以帮助我们实现这样的需求。

    // customEquals: lodash.isEqual、Immutable.is、dequal.deepEqual 等;
    const useOriginalCopy = (value) => {
      const copy = React.useRef();
      const diffRef = React.useRef(0);
      if (!customEquals(value, copy.current)) {
        copy.current = value;
        diffRef.current += 1;
      }
      return [diffRef.current];
    };

总结

关于 React 项目中的 reRender 优化一直是个老生常谈的问题,大家在项目中或多或少都能总结出自己的经验,如批量更新、不透传 props 、使用发布订阅模式等。而且在 React 推崇的函数式编程中,通常情况下一个组件的代码量不宜过多,这也就更多的要求开发者将组件细化,而更容易的控制组件的属性与状态,当你迷惑为什么发生 reRender 的时候,React Profiler 是一个答案。

参考资料

React 性能优化

React Profiler 介绍

Use the React Profiler for Performance

用 React Hooks 和调试工具提升应用性能

React Issuse 16221

15 分钟学会 Immutable

❉ 作者介绍 ❉