这是第 115 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:React Profiler 的使用
React Profiler 的使用
前言
平时大家开发项目的时候,有时候会感觉项目卡顿,通常情况下可以即时做出整改,但也有时候不太容易找到引起卡顿的点,或者说不好发现潜在的性能问题,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
生成如下的信息:
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
用在其子级的渲染上。 子组件 Display
和 Count
也有自己对应的渲染时间,以此类推。
- 组件的宽度及颜色表示渲染所耗费的时间,同样是黄色时间较长;
为了更方便的查看组件的耗时,我们可以切换 Ranked 排序图
,可以很清楚的看到耗费时间最长的那个组件。
例如 10/11 这次提交,操作上只是点击了 add button 来更新 Count
, 但是这里 Display
却是最耗时的那个。
- 单击选中
Display
,可以在右侧看到 6 次rendered
信息, 上方的Why did this render?
记录了每次rendered
的原因;
如果你非常了解这里的代码,可以非常容易想到下一步就是优化 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
的不必要渲染。
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
是一个答案。
参考资料
Use the React Profiler for Performance
推荐阅读
开源作品
- 政采云前端小报
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com