👆 这是第 115 篇不掺水的原创,想要了解更多,请戳上方蓝色字体:政采云前端团队 关注我们吧~
本文首发于政采云前端团队博客:React Profiler 的使用 https://zoo.team/article/use-of-react-profiler
平时大家开发项目的时候,有时候会感觉项目卡顿,通常情况下可以及时做出整改,但也有时候不太容易找到引起卡顿的点,或者说不好发现潜在的性能问题,React Developer Tools
提供的 Profiler
可以直观的帮助大家找出 React 项目中的性能瓶颈,进一步来改善我们的应用,推荐给大家安装使用。
可以从 Chrome 应用市场、Firefox 浏览器扩展、Node 包 下载安装;
react 16.5+ 开发模式下才可以使用该功能,生成环境使用请移步官方文档 (https://fb.me/react-profiling) 。
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 列则对应了我们上面的步骤操作:
左右切换 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 更新数据的地方。嗯,满意!
针对不同的业务场景,这里的比较函数会有不同的写法,比如仅仅比较 props
的某个属性,或与本文中的例子一样以 JSON.stringify
来直接比较 props
。对于复杂的数据结构,如果需要阻止 reRender
,不建议进行深层比较或者使用 JSON.stringify
,这样非常影响效率。可以考虑使用 immutable (https://immutable-js.com/) 来加速嵌套数据的比较,关于 immutable
的使用,可以查看 15 分钟学会 Immutable。你可以去实现自己的 CustomComponent
,以达到和 PureComponent
一样的使用方式和目的。
shouldComponentUpdate
视为提示而不是严格的指令,并且当返回 false
时,仍可能导致组件重新渲染 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
依靠 shouldComponentUpdate
实现了一层 shallowEqual (https://reactjs.org/docs/shallow-compare.html),仅作对象的浅层比较,以减少跳过更新的可能性,但是如果对象中包含复杂的数据结构,则有可能产生错误的比对,所以 PureComponent
会更多的运用于较为简单的 props & state
展示组件上。
React.memo
与其原理一样,只是用于 函数组件
上,回调函数的返回值与 shouldComponentUpdate
相反;
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
是一个答案。
React 性能优化 (https://react.docschina.org/docs/optimizing-performance.html)
React Profiler 介绍 (https://react.docschina.org/blog/2018/09/10/introducing-the-react-profiler.html)
Use the React Profiler for Performance (https://scotch.io/tutorials/use-the-react-profiler-for-performance)
用 React Hooks 和调试工具提升应用性能 (https://github.com/xitu/gold-miner/blob/master/TODO1/increase-your-apps-performance-with-react-hooks-and-the-react-dev-tools.md)
React Issuse 16221 (https://github.com/facebook/react/issues/16221)
看完两件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我两件小事
1.点个「在看」,让更多人也能看到这篇内容(点了「在看」,bug -1 😊)
2.关注公众号「政采云前端团队」,持续为你推送精选好文
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com