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

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

渲染阶段: 会确定需要进行哪些更改,比如 DOM 。在此阶段 React 调用 render,然后将结果与上次渲染的结果进行比较。
提交阶段 :发生在 React 应用变化时。在此阶段 React 还会调用 componentDidMount 和 componentDidUpdate 之类的生命周期方法。( 对于 React DOM 来说,会发生在 React 插入,更新及删除 DOM 节点的时候。)

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

使用
安装
可以从 Chrome 应用市场Firefox 浏览器扩展Node 包 下载安装;
react 16.5+ 开发模式下才可以使用该功能,生成环境使用请移步 官方文档 。

介绍
下图为面板按钮基本功能

打开设置可以记录组件 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 用在其子级的渲染上。 子组件 Display 和 Count 也有自己对应的渲染时间,以此类推。

组件的宽度及颜色表示渲染所耗费的时间,同样是黄色时间较长;
5.为了更方便的查看组件的耗时,我们可以切换 Ranked 排序图,可以很清楚的看到耗费时间最长的那个组件。
6.例如 10/11 这次提交,操作上只是点击了 add button 来更新 Count, 但是这里 Display 却是最耗时的那个。

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

<> 可以查看源码;
🐞 可以在控制台打印组件信息;
阻止重新渲染
改变 Display 和 Count 的写法,保证两个组件 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>;
});
再重复执行一次上面的操作,看一下结果。

很遗憾,虽然 Display 在 React.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 的不必要渲染。
// 堆代码 duidaima.com
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 提供的诸如 useEffect、useMemo、useCallback 等钩子函数,他们都带有 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 是一个答案。
用户评论