沈炼.jpg

> 这是第 115 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:React Profiler 的使用open in new window

React Profiler 的使用

前言

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

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

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

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

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

使用

安装

可以从 Chrome 应用市场open in new windowFirefox 浏览器扩展open in new windowNode 包open in new window 下载安装;

react 16.5+ 开发模式下才可以使用该功能,生成环境使用请移步 官方文档open in new window

介绍

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

  • 打开设置可以记录组件 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 生成如下的信息:

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

  • 列的颜色和高度对应该次提交渲染所需的时间 (较高的黄色比较短的绿色耗费时间长);

  • 我们可以忽略掉最短的灰色列,灰色代表没有重新渲染;

A 区较高的 6 列则对应了我们上面的步骤操作:

  • 第一列对应页面的 mount ,因为是首次渲染,所以最高,代表耗时最长;

  • 第二、三列对应了 input 输入文字引发的两次渲染;

  • 最后三列则对应了 add button 三次点击引发的渲染。

左右切换 A 区的数据,表示了选中列的提交信息就会展示在 B 区,同时在 C 区展示应用程序内组件(如 Display 、Count )的详细信息。

  • Committed at 表示相对于本次 record 的时间,可以忽略;

  • Render duration 表示本次提交渲染耗时,我们需要关注这个;

例如 06/11 这次提交,整个 Anonymous 组件用了 1ms 来渲染, 但本身只耗费了 0.2ms,即图中的 0.2ms of 1ms,剩余的 0.8ms 用在其子级的渲染上。 子组件 DisplayCount 也有自己对应的渲染时间,以此类推。

  • 组件的宽度及颜色表示渲染所耗费的时间,同样是黄色时间较长;

为了更方便的查看组件的耗时,我们可以切换 Ranked 排序图,可以很清楚的看到耗费时间最长的那个组件。

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

  • 单击选中 Display,可以在右侧看到 6 次 rendered 信息, 上方的 Why did this render? 记录了每次 rendered 的原因;

如果你非常了解这里的代码,可以非常容易想到下一步就是优化 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,这样非常影响效率。可以考虑使用 immutableopen in new window 来加速嵌套数据的比较,关于 immutable 的使用,可以查看 15 分钟学会 Immutableopen in new window。你可以去实现自己的 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 实现了一层 shallowEqualopen in new window,仅作对象的浅层比较,以减少跳过更新的可能性,但是如果对象中包含复杂的数据结构,则有可能产生错误的比对,所以 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 性能优化open in new window

React Profiler 介绍open in new window

Use the React Profiler for Performanceopen in new window

用 React Hooks 和调试工具提升应用性能open in new window

React Issuse 16221open in new window

15 分钟学会 Immutableopen in new window

推荐阅读

电商最小存货 - SKU 和 算法实现open in new window

你需要知道的项目管理知识open in new window

如何从 0 到 1 搭建代码全局检索系统open in new window

如何搭建适合自己团队的构建部署平台open in new window

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/open in new window (小报官网首页有微信交流群)

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com