React Profiler 的使用

React Profiler 的使用

沈炼.jpg

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

React Profiler 的使用

前言

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

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

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

    • 提交阶段 发生在 React 应用变化时。在此阶段 React 还会调用 componentDidMountcomponentDidUpdate 之类的生命周期方法。( 对于 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 生成如下的信息:

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,这样非常影响效率。可以考虑使用 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

推荐阅读

电商最小存货 - SKU 和 算法实现

你需要知道的项目管理知识

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

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

开源作品

  • 政采云前端小报

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

招贤纳士

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

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

分类:
前端