关于 React 性能优化的那些事
我报名了金石计划的第一个挑战——瓜分10万奖池,这是我的第一篇文章,点击查看活动详情
要明确性能优化的原理,需要了解它的前世今生,需要回答以下问题:
- React 如何渲染页面?
- 导致页面卡顿的罪魁祸首是什么?
- 为什么我们需要性能优化?
- React 中有哪些需要性能优化的场景?
- React 自己的性能优化方法?
- 还有哪些工具可以提高性能?
为什么页面冻结?
为什么浏览器会出现页面冻结问题?浏览器不够先进?都2202年了,怎么还有这样的问题?
其实问题的根源在于浏览器的刷新机制。
我们人眼的刷新率为60Hz,浏览器根据人眼的刷新率计算刷新率。
1000 毫秒 / 60 = 16.6 毫秒
也就是说,如果浏览器需要16.6Ms刷新一次,人眼是不会感觉到卡顿的,如果刷新超过这个时间,就会感觉到卡顿。
浏览器的主进程只需要渲染页面,还需要解析和执行Js,它们在一个进程中运行。
如果js的执行长时间占用主进程的资源,就会没有资源来渲染和刷新页面,从而导致页面卡顿。
那么这和 React 性能优化有什么关系呢?
React 的滞后在哪里?
据我们了解,js长时间占用浏览器主线程,导致无法刷新导致卡顿。
那么 React 的滞后也是基于这个原因。
React 在渲染时,会将已有的 render 生成的新的 jsx 数据与已有的 fiberRoot 进行对比,找出差异,然后生成新的 workInProgress,然后在 mount 阶段将新的 workInProgress 交给服务器进行渲染。
在这个过程中,为了让底层机制更加高效快捷,React进行了很多优化处理,比如设置任务优先级、异步调度、diff算法、时间分片等。
整个环节是为了高效快速的完成从数据更新到页面渲染的整个流程。
为了防止查找所有更新节点的递归遍历过大,占用浏览器资源,React 升级了 Fiber 架构和时间分片,使其可以增量更新。
为了找到所有更新的节点,建立了一个diff算法来有效地找到所有节点。
为了更高效地更新和及时响应用户操作,设计任务调度优先级。
而我们的性能优化是不拖 React 后退,让遍历更快更高效。
那么性能优化是什么意思呢? ?
就是控制刷新渲染的范围。我们只允许更新的更新,不应该更新的需要更新。让我们的更新链接尽可能短的完成,那么页面当然会及时刷新,不会卡顿。
React 中有哪些需要性能优化的场景?
- 父组件刷新不影响子组件
- 组件控制是否自行刷新
- 减少影响范围,不相关的刷新数据不存储在状态
- 合并状态以减少重复的 setState 操作
- 如何更快地完成diff比较,加快进程
让我们分别谈谈这些场景:
一:父组件刷新,不影响子组件。
我们知道React会深入遍历所有子组件,找到所有更新的节点,根据新的jsx数据和旧的fiber生成新的workInProgress,然后进行页面渲染。
那么如果父组件刷新了,子组件也必然会被刷新,但是如果这个刷新与我们的子组件无关呢?如何减少这种传播?
如下:
导出默认函数Father1(){
让 [name,setName] = 反应。使用状态('');
返回 (
<div>
< button onClick = {() => setName("获取数据")}>点击获取数据</ button >{name} < 孩子 />
</ div >
)
}
函数儿童(){
返回 (
<div> 这里是子组件</ div >
)
}
复制代码
运行结果:
可以看出我们的子组件受到了影响。解决方法很多,一般分为两种。
- 子组件决定是否需要更新,一般是 PureComponent、shouldComponentUpdate、memo
- 父组件对子组件做缓冲判断
第一种:使用 PureComponent
使用 PureComponent 的基本原理是它对 state 和 props 进行浅层比较,如果它们不同则更新。
导出默认函数Father1(){
让 [name,setName] = 反应。使用状态('');
返回 (
<div>
< button onClick = {() => setName("父组件的数据")}>点击刷新父组件</ button >{name} <儿童1 />
</ div >
)
}
类儿童扩展 React.PureComponent{
使成为() {
返回 (
<div> 这里是子组件</ div >
)
}
}
复制代码
结果:
实际上 纯组件
也就是在调用内部update的时候,会调用下面的方法来判断新旧state和props
函数 shallowEqual(objA:混合,objB:混合):布尔 {
如果(是(objA,objB)){
返回真;
}
如果 (
typeof objA !== '对象' ||
objA === 空 ||
typeof objB !== '对象' ||
objB === 空
) {
返回假;
}
常量键A = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
返回假;
}
// 测试 A 与 B 不同的键。
for ( 让 i = 0; i < keysA.length; i++) {
常量 currentKey = keysA[i];
如果 (
!hasOwnProperty。调用(objB, currentKey) ||
!是(objA[currentKey],objB[currentKey])
) {
返回假;
}
}
返回真;
}
复制代码
其判断步骤如下:
- 第一步是直接比较新旧
道具
或新旧状态
是平等的。如果相等,则不更新组件。 - 第二步,判断新旧
状态
或者道具
,它不是一个对象或者是无效的
, 然后直接返回 false 来更新组件。 - 第三步,通过
对象键
把新的和旧的道具
或新旧状态
的财产名称钥匙
变成一个数组,判断数组的长度是否相等。如果不相等,则证明属性有增减,然后更新组件。 - 第四步,遍历老
道具
或旧状态
,判断对应的新道具
或新的状态
,是否有对应和相等(这个相等是浅比较),如果有不对应或者不相等,则直接返回错误的
, 更新组件。至此,浅层比较过程结束,纯组件
这就是渲染节流优化的目的。
使用 PureComponent 时要注意的细节:
因为 纯组件
浅层比较判断 状态
和 道具
,所以如果我们在一个父子组件中,子组件使用 纯组件
, 在父组件刷新过程中,不小心改变了传递给子组件的回调函数,会导致子组件误触发。此时 纯组件
将失败。
细节一:在函数组件中,匿名函数、箭头函数和普通函数会被重新声明
以下情况会导致函数重新声明:
箭头函数
<Children1 callback={(value)=>设置值(值)}/>
复制代码
匿名函数
<Children1 callback={function (value){ setValue(value)}}/>
复制代码
普通功能
导出默认函数Father1(){
让 [name,setName] = 反应。使用状态('');
让 [value,setValue] = 反应。使用状态('')
常量 setData=( 值)=>{
设置值(值)
}
返回 (
<div>
< button onClick = {() => setName("父组件的数据"+Math.random())}>点击刷新父组件</ button >{name} < Children1 回调 = {setData}/ >
</ div >
)
}
类 Children1 扩展 React.PureComponent{
使成为() {
返回 (
<div> 这里是子组件</ div >
)
}
}
复制代码
结果:
可以看到子组件的 PureComponent 完全失效了。这时候可以使用 useMemo 或者 useCallback 出去,用它们来缓冲一个函数,保证不会有重复声明。
导出默认函数Father1(){
让 [name,setName] = 反应。使用状态('');
让 [value,setValue] = 反应。使用状态('')
常量 setData= 反应。使用回调((值)=>{
设置值(值)
},[])
返回 (
<div>
< button onClick = {() => setName("父组件的数据"+Math.random())}>点击刷新父组件</ button >{name} < Children1 回调 = {setData}/ >
</ div >
)
}
复制代码
查看结果:
可以看到我们的子组件这次没有参与父组件的刷新。 反应探查器
还提醒, 儿童1
不渲染。
细节二:类组件中不使用箭头函数,匿名函数
原理和函数组件中一样,类组件中的每次刷新都会被重复调用 使成为
函数,那么 使成为
函数中使用匿名函数和箭头函数会导致重复刷新问题。
导出默认类 Father 扩展 React 。纯组件{
构造函数(道具){
超级(道具);
这个.state = {
姓名: ””,
数数: ””,
}
}
使成为() {
返回 (
<div>
<button onClick={()=> this.setState({name: "Data of parent component"+ Math.random()})}>点击获取数据</button>
{ this.state.name}
< Children1 回调={()=> this.setState({count: 11})}/>
</div>
)
}
}
复制代码
结果:
而且优化这个很简单,把函数换成普通函数就行了。
导出默认类 Father 扩展 React 。纯组件{
构造函数(道具){
超级(道具);
这个.state = {
姓名: ””,
数数: ””,
}
}
setCount=(计数)=>{
this.setState({count})
}
使成为() {
返回 (
<div>
<button onClick={()=> this.setState({name: "Data of parent component"+ Math.random()})}>点击获取数据</button>
{ this.state.name}
< Children1 回调={ this.setCount(111)}/>
</div>
)
}
}
复制代码
结果:
细节3:类组件的render函数中的bind函数
这个细节是我们在类组件中,而不是在 构造函数
进行中 绑定
操作,但在 使成为
函数,那么由于 绑定
函数的特性,每次调用都会返回一个新的函数,所以也会导致 纯组件
失败
导出默认类 Father 扩展 React 。纯组件{
//...
设置计数(计数){
this.setCount({count})
}
使成为() {
返回 (
<div>
<button onClick={()=> this.setState({name: "Data of parent component"+ Math.random()})}>点击获取数据</button>
{ this.state.name}
< Children1 回调={ this.setCount.bind(this, "11111")}/>
</div>
)
}
}
复制代码
查看执行结果:
优化的方式也很简单,把 绑定
操作 构造函数
在里面。
构造函数(道具){
超级(道具);
这个.state = {
姓名: ””,
数数: ””,
}
this.setCount= this.setCount.bind(this);
}
复制代码
此处不显示执行结果。
第二种:shouldComponentUpdate
在类组件中使用 shouldComponentUpdate 是主要的优化方法,它不仅可以判断 下一个道具
, 也根据 下一个状态
和最新的 下一个上下文
来决定是否更新。
Children2 类扩展了 React。纯组件{
shouldComponentUpdate(nextProps, nextState, nextContext) {
//判断只有偶数的时候,子组件才会更新
if(nextProps !== this.props && nextProps.count % 2 === 0){
返回真;
} 别的{
返回假;
}
}
使成为() {
返回 (
<div>
只有父组件传入的值等于 2的时候才会更新
{ this.props.count}
</div>
)
}
}
复制代码
它的用法也很简单,就是需要更新就返回true,不需要更新就返回false。
第三:功能组件如何判断props变化的更新?使用 React.memo 函数
反应备忘录
规则是,如果要重用最后的渲染结果,返回 真的
, 不想重复使用就返回 错误的
.
所以它和 应该组件更新
相反, 错误的
将会被更新, 真的
返回缓冲区。
const Children3 = 反应。备忘录(函数({count}){
返回 (
<div> {count} 只有在父组件传入的值为偶数时才会更新</ div >
)
}, ( prevProps, nextProps )=>{
if(nextProps.count % 2 === 0){
返回假;
} 别的{
返回真;
}
})
复制代码
如果我们不传入第二个函数,而是让 反应备忘录
包裹它,然后它只会 道具
浅比较,没有可比性 状态
这样的逻辑。
以上三个是我们响应父组件的更新触发子组件,子组件决定是否更新的实现。
先说一下父组件是如何缓冲子组件的:
使用 React.useMemo 缓冲子组件
看下面的逻辑,我们的子组件只关心 数数
刷新时的数据 姓名
加载数据时,不会触发刷新 儿童1
子组件,它实现了我们对组件的缓冲区控制。
导出默认函数Father1(){
让 [count,setCount] = 反应。使用状态(0);
让 [name,setName] = 反应。使用状态(0);
常量渲染 = 反应。 useMemo( ()=> < Children1 count = {count}/ >,[count])
返回 (
<div>
< button onClick = {() => setCount(++count)}>点击刷新计数</ button >
<br />
< button onClick = {() => setName(++name)}>点击刷新名称</ button >
< br /> {“计数”+计数} < br /> {“名称”+名称} < br /> {渲染}</ div >
)
}
类 Children1 扩展 React.PureComponent{
使成为() {
返回 (
<div> 子组件只涉及计数数据{this.props.count}</ div >
)
}
}
复制代码
结果:
当我们点击刷新名称数据时,可以看到刷新没有涉及到子组件
当我们点击刷新计数数据时,子组件参与刷新
二:组件控制是否自行刷新
这里有必要使用上面提到的 应该组件更新
也 纯组件
,这里不再赘述。
3:减小影响范围,不相关的刷新数据不存储在状态
这种情况是我们有意识的控制。如果页面上有我们不用的数据,但是和我们的其他逻辑有关,那么我们可以把它存放在其他地方而不是状态中间。
场景一:无意义的重复调用setState合并相关状态
导出默认类父扩展 React.Component{
状态 = {
计数:0,
姓名: ””,
}
获取数据=(计数)=>{
这个。 setState({count});
// 异步获取数据
设置超时(()=>{
这个。设置状态({
name: "异步获取返回数据"+count
})
}, 200)
}
componentDidUpdate(prevProps, prevState, 快照) {
安慰。 log("渲染时间",++count, "二等")
}
使成为() {
返回 (
<div>
< button onClick = {() => this.getData(++this.state.count)}>点击获取数据</ button >{this.state.name}</ div >
)
}
}
复制代码
反应探查器
执行结果:
//掘金
可以看到我们的父组件执行了两次。
其中一个首先是没有意义的 设置状态
将数据保存一次,根据此数据异步获取数据后再次调用 设置状态
,导致第二次数据刷新。
解决的办法是在异步数据获取完成后将这些数据合并成状态。
获取数据=(计数)=>{
// 异步获取数据
设置超时(()=>{
这个。设置状态({
name: "异步获取返回数据"+count,
数数
})
}, 200)
}
复制代码
看执行结果:只渲染一次。
场景二:没有与页面刷新相关的数据,没有存储在 state 中
事实上,我们发现这个数据并没有显示在页面上,我们不需要将它们全部存储在状态中,因此我们可以将这些数据存储在状态之外的地方。
导出默认类父扩展 React.Component{
构造函数(道具){
超级(道具);
这个。状态 = {
姓名: ””,
}
这个。计数 = 0;
}
获取数据=(计数)=>{
这个。计数=计数;
// 异步获取数据
设置超时(()=>{
这个。设置状态({
name: "异步获取返回数据"+count,
})
}, 200)
}
componentDidUpdate(prevProps, prevState, 快照) {
安慰。 log("渲染时间",++count, "二等")
}
使成为() {
返回 (
<div>
< button onClick = {() => this.getData(++this.count)}>点击获取数据</ button >{this.state.name}</ div >
)
}
}
复制代码
这样的操作不影响我们的使用。
存在 班级
在组件中,我们可以将数据存储在 这个
上面,而在 功能
,那么我们可以使用 使用参考
这个 挂钩
达到同样的效果。
导出默认函数Father1(){
让 [name,setName] = 反应。使用状态('');
const countContainer = 反应。使用参考(0);
常量 getData=( 计数)=>{
// 异步获取数据
设置超时(()=>{
setName("异步获取返回数据"+count)
计数容器。当前=计数++;
}, 200)
}
返回 (
<div>
< button onClick = {() => getData(++countContainer.current)}>点击获取数据</ button >{姓名}</ div >
)
}
复制代码
场景三:通过将数据存储在useRef中,避免父子组件重复刷新
假设父组件有数据需要在子组件中使用,子组件需要将数据返回给父组件,如果父组件将这些数据存储在 统计
e 中,则刷新父组件,子组件也会刷新。
在这种情况下,我们可以将数据存储在 使用参考
, 避免无意义的刷新。或者将数据存储在类中 这个
下。
四:合并状态减少重复的setState操作
合并 状态
, 减少重复 设置状态
操作,其实 反应
已经为我们做好了,也就是批量更新,在 反应18
在之前的版本中,批量更新只能在 React 自己的生命周期或者点击事件中使用,而异步更新不可用,比如 设置超时
, 设置内部
等待。
所以如果我们想 反应18
在之前的版本中,如果要在异步代码中添加对批量更新的支持,可以使用 反应
提供给我们 api
.
从 'react-dom' 导入 ReactDOM;
常量 { 不稳定的批处理更新 } = ReactDOM;
复制代码
使用方法如下:
组件DidMount() {
设置超时(()=>{
不稳定的批处理更新(()=> {
this.setState({ number: this.state.number + 1 })
console.log(this.state.number)
this.setState({ number: this.state.number + 1})
console.log(this.state.number)
this.setState({ number: this.state.number + 1 })
console.log(this.state.number)
})
})
}
复制代码
五:如何更快地完成diff比较,加快进程
差异
算法是为了帮助我们找到需要更新的异同点,那么有没有什么办法可以让我们的 差异
更快的算法呢?
这是合理使用 钥匙
差异
电话在 调和孩子
中间 reconcileChildFibers
, 当没有可重用的 当前的`` 纤维
节点,它会去 mountChildFibers
, 离开时 reconcileChildFibers
.
和 reconcilerChildFibers
在函数中,针将是 使成为
函数返回的新的 jsx
数据来判断,是否是一个物体,它会判断它的 newChild.$$typeof
不管与否 REACT_ELEMENT_TYPE
,如果是,则作为单个节点处理。如果不是,继续判断是否是 REACT_PORTAL_TYPE
或者 REACT_LAZY_TYPE
.
继续判断是数组,还是可迭代对象。
并且在单节点处理函数中 协调单元素
,将执行以下逻辑:
-
经过
钥匙
, 判断上次更新的时间纤维
是否有对应节点DOM
节点。
如果没有,直接进入创建过程,生成新的Fiber节点,返回 -
如果有,那么继续判断,
DOM
节点是否可重用? -
如果有,最后更新的
纤维
节点的副本用作新的纤维
节点并返回 -
如果没有,那么标记
DOM
需要删除,生成一个新的纤维
节点并返回。函数 reconcileSingleElement ( returnFiber: Fiber , currentFirstChild: Fiber | null , element: ReactElement ): Fiber {
常量键 = element.key; //jsx虚拟DOM返回的数据
让孩子 = currentFirstChild; //当前光纤// 首先判断是否有对应的DOM节点
而(孩子!==空){
// 上次更新有DOM节点,然后判断是否可以复用// 首先检查键是否相同
if (child.key === key) {// 键相同,然后比较类型是否相同
开关(child.tag){
// ...省略大小写默认: {
if (child.elementType === element.type) {
// 相同类型表示可以复用
// 返回复用的光纤
返回现有的;
}// 如果类型不同,跳出开关
休息;
}
}
// 这里执行的代码意思是:key相同但类型不同
// 将此纤程及其兄弟纤程标记为删除
deleteRemainingChildren(returnFiber, child);
休息;
} 别的 {
// 如果key不同,则标记要删除的纤程
deleteChild(returnFiber, child);
}
孩子=孩子。兄弟姐妹;
}// 创建一个新的 Fiber,并返回... 省略
}
复制代码
从上面的代码可以看出, 反应
如何判断一个 纤维
节点是否可以重用。
- 第 1 步:判断
元素
的钥匙
和纤维
的钥匙
是不是一样 - 如果不一样,将创建一个新的
纤维
, 并返回 - 第二步:如果相同,判断
元素类型
和纤维
的类型
是一样的,类型
是他们的类型,比如p
标签是p, 格
标签是div
。如果类型
如果它们不相同,它们将被标记为删除。 - 如果相同,则可以判断可以重复使用,返回
现存的
.
更新多个节点时, 钥匙
更重要的是, 反应
通过遍历新旧数据、数组和链表来判断它们 钥匙
和 类型
决定是否重复使用。
所以我们需要明智地使用它 钥匙
加快 差异
算法对齐和 纤维
重用。
那么如何明智地使用它 钥匙
羊毛布。
其实很简单,每次只需要设置值和我们的数据即可。不使用 大批
下标,这个 钥匙
与数据无关,我们的数据已经更新,结果 反应
还指望重用。
还有哪些工具可以提高性能?
在实际开发中,汽油还有很多其他场景需要优化:
- 针对频繁打字或滑动滚动的防抖节流
- 用于大数据演示的虚拟列表、虚拟表
- 大数据展示的时间切片
- 对于反应
感谢提供这篇好文章:
超过…
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。
这篇文章的链接: https://homecpp.art/5024/10565/1606
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明