一个用于解决 React 常见问题的 Checklist
原文地址:https://logrocket-blog.ghost.io/death-by-a-thousand-cuts-a-checklist-for-eliminating-common-react-performance-issues/
这是一份非常实用的,一步步解决 React
性能问题的清单。
你想不想让你的 React
应用程序运行更快?
想不想有一份清单来检查常见的 React
性能问题?
如果你的答案,都是 yes~那就抓紧时间来读一下这篇文章。这是一份非常实用的解决 React
性能问题的清单
在本文中,我将为大家介绍一个逐步消除常见性能问题的指南。
我会先描述问题,然后再给出解决方案。在这同时,你可以将同样的问题带入到你自己的项目中审查。
下面就开始动手吧~
项目
为了大家都方便学习,我会在同一个 React
应用上来介绍各种问题的情况。
项目名称为 Cardie
可以在 Github
上下载Cardie
源码,跟着我一起学习。
Cardie
是一个功能非常简单的项目。就是常见的个人简介页,用来展示一个用户的个人介绍。
页面内包含一个可点击的按钮,可以操作更改用户的职业信息。
Cardie in Action ?
点击按钮,用户的职业信息会发生改变。
看到这里,你可能会有点忍不住想笑了。这个应用相比于真实的项目也太过于简单了。这个的项目的性能问题,能和我们真实世界的应用比吗?
现在,我们继续。
1. 辨别无用的渲染
解决性能问题第一步,最好的方式就是从分辨哪些是你项目里,没有用的渲染。
检查方式多种多样,但最简单的方式,就是打开你的React devtools
面板,切换 highlight updates
这个选项。
如何在 React devtools
中开启 highlight updates
这个时候,App 上更新渲染的地方,会有一个闪烁。
在蒙层下面的组件,会被 React
重新渲染。
点击切换职业,会发现,整个父级 App
都被重新渲染了。
注意用户卡片边缘的闪烁
这就不太对了。
可以看到 App
虽然运行正常,但是更改很小的地方,不应该整个组件都重新渲染啊。
实际发生改变的是 App
内很小的一部分
理想的更新应该像下面这样:
注意更新只发生在很小的区域内
无用的重新渲染对于复杂的项目来说,更是会引发不小的性能问题。
发现问题了吗?解决了吗?
2. 将需要频繁更新的区域单独创建为组件
一旦在你的应用里,发现了无用的渲染,重新去整理你的组件树,是非常好的解决方式。
下面我们来详细说明一下。
在 Cardie
中,App
组件通过 react-redux
里的 connect
方法连接到 redux store
。从 store
中获取属性:name
, location
, likes
和 description
。
<App/>
直接操作 redux store
获取数据
在个人介绍页目前定义了 description
属性。
原因就是因为,点击按钮的时候, description
属性发生了改变。改变引发了 App
组件的重新渲染。
记不记得 React 101
里有句话,一个组件中无论是 props
还是 state
发生改变,都会触发重新渲染。
React 组件的元素渲染树。这些元素是通过 props
和 state
定义的。如果 props
或者 state
发生改变,元素树会重新渲染。结果就会是一个新的树。
我们应该如何让一个特定的 React
组件元素更新,而不是 App
组件?
例如: 我们可以创建一个新组件,名字叫 Profession
。渲染它自己的元素。
在这个例子里,如果将职业点击切换为“我是一个程序员”的时候, Profession
组件会被重新渲染。
在 组件内的 组件会重新渲染
新的组件树如图:
组件渲染了包括 组件的元素
也就是说之前是 组件在关心 profession
属性,现在变成了 <Profession/>
这个组件的事情。
组件 <Profession/>
会直接从 redux store
获取 profession
属性。
不管你是否使用了 redux
,这里的关键点是 App
组件不再会因为 profession
属性的改变,而引发重绘。而是被组件 <Profession/>
取代了。
更改之后,我们再来看一下 update highlighted
的表现:
注意只有 <Profession />
内部发生了更新
详细的代码更改,可以看项目的这个分支 isolated-component branch
3. 在恰当的地方使用静态组件
React
提到性能问题,不得不说的就是静态组件。那么,怎样正确使用静态组件
当然,你可以把每个组件都写成静态组件,但是你要记得,有一个方法特殊。 shouldComponentUpdate
方法。
我们假定只有 props
和之前的 props
和 state
不相同的时候,会引发组件的重新渲染。
和 React.PureComponent
相对的就是默认的 React.Component
组件。
用 React.PureComponent
替代 React.Component
为了解释 Cardie
项目里,对于这个具体使用的区别,
我们将 Profession
组件插入更小的其他组件。
目前,我们的 Profession
代码是这样的:
constDescription=({ description })=>{return(
<p>
<span className="faint">I am</span> a {description}
</p>
);
}
我们想改成这样
constDescription=({ description })=>{return(
<p>
<I/>
<Am />
<A/>
<Profession profession={description}/>
</p>
);
};
这样,Description
组件就会有 4 个子组件。
注意 Description
组件里的 profession
属性,它传递这个属性值给到 Profession
组件。从理论上来说,其他三个组件不需要关心 profession
的值。
我们假设新的子组件内容是非常简单的。<I />
组件 就返回一个 span
元素,内部包裹着一个字母 I
, <span >I </span>
。
项目可以正常运行,结果也没有问题。
但是当你修改 description
这个属性的时候,Profession
下所有的子组件都跟着重新渲染了
一旦有新的属性值传递给 Description
组件时,其他子组件也会被重新渲染。
我在render
方法里加了日志输出,我们可以真实的在控制台看到,每个子组件都被重新渲染了。
你也可以在 react dev tools
里查看 highlighted updates
。
注意这几个词 I, am
和 a.
都被重新渲染过
这是意料之中的事情。无论是 props
还是 state
发生改变,元素树都会被重排。
和重绘是一样的。
在这个例子里,我们需要提前约定好, <I/>, <Am/>
和 <A/>
三个组件是不需要重绘的。没错, props
来自父组件,<Description/>
变化了。这是不可避免的。但是假设我们的应用足够大,这个现象就会引发构成性能威胁。
假设我们使用静态组件作为子组件呢?
<I />
组件:
import React,{Component, PureComponent}from"react"//before
classIextendsComponent{
render(){
return<span className="faint">I</span>;
}
//after
classIextendsPureComponent{
render(){
return<span className="faint">I</span>;
}
补充说明一下, React
通过 hood
通知到这些子组件,即使属性值有变化,他们也不需要更新渲染。
就是说无论父元素属性值如何变化,都不重新渲染!
了解原理之后,我们来看 highlighted updates
, 如图所示,子组件不再重新渲染了。prop
值变化时,只有 Profession
组件重新渲染了。
在字母 “I”, “am” and “a”.
这三个周围没有出现边框,只有包裹的容器,profession
组件闪烁。
在更大型的应用里,通过设置为静态组件,可能可以大幅度提升性能。
想查看代码改动,可以看项目分支 pure-component branch
4. 避免给 props
传新对象
再次强调一下,props
变化,会导致组件重新渲染。
不管是 props
还是 state
发生变化,VDOM 树都会重新渲染,导致生成新的 VDOM 树
万一你的组件没改变 props
, 但 React
认为它改变了呢?
是的,也会重新渲染!
但是这不是很奇怪吗?
这种情况的发生是和 javascript
的运行原理, React
处理新旧值比较的实现方式是有关系的。
让我们看一个例子。
下面是 Description
组件的代码内容:
constDescription=({ description })=>{return(
<p>
<I/>
<Am />
<A/>
<Profession profession={description}/>
</p>
);
};
接下来,我们给 I
组件定义一个 i
属性值。定义为一个对象:
const i ={value:"i"
};
不管 value 里,具体值是什么,我们都可以直接取变量名渲染到组件 I
中。
classIextendsPureComponent{render(){
return<span className="faint">{this.props.i.value}</span>;
}
}
在组件 Description
中,属性 i
将会如下定义和使用:
classDescriptionextendsComponent{render(){
const i ={
value:"i"
};
return(
<p>
<I i={i}/>
<Am />
<A/>
<Profession profession={this.props.description}/>
</p>
);
}
}
这段代码可以正常运行,但是这里面有一个问题,不知道你有没有注意到?
尽管 I
是一个静态组件,但是现在用户的职业变更时,它也跟着重新渲染了
点击按钮,会发现 <I/>
和 <Profession/>
组件都重新渲染了。但是<I/>
组件真实的属性并没有变化啊,为什么会这样?
这是为什么呢?
Description
组件接收了一个新的属性值, render
方法被调用生成了新的 VDOM
树。
为了给 render
方法传参,定义了一个新常量 i
:
const i ={value:"i"
};
当 React
执行这行代码的时候,<I i={i} />
,它将 i
当作一个不同的属性,一个新的对象进行传参的,所以重新渲染了。
如果你记得 React 101
, React
会对新旧 props
进行对比。
像字符串和数字这种值类型的,是针对值来进行对比。对象类型是引用对比。
尽管常量 i
每次都是渲染相同的值,但是引用地址不同,内存不记录它上次的位置。
每次渲染都会被新建对象,所以, prop
每次传给 <I />
组件的都是”新“ 的对象,所以不断的重新渲染。
在一个大型应用场景中,这会导致无用渲染,导致潜在的性能陷阱。
如何避免呢?
可以给每一个 prop
加一个事件处理。
如果你想避免这种情况的发生,你就不能这样写:
...render(){
<div onClick={()=>{//do something here}}
}
...
你可以新建一个方法对象,每次渲染的时候去调用方法。像这样:
...handleClick:()={
}
render(){
<div onClick={this.handleClick}
}
...
明白了吗?
同样,我们需要把 prop
传递给 <I>
classDescriptionextendsComponent{i ={
value:"i"
};
render(){
return(
<p>
<I i={this.i}/>
<Am />
<A/>
<Profession profession={this.props.description}/>
</p>
);
}
}
这样的话,引用就是同一个了, this.i
。
渲染的时候就不会有新对象了。
同样,代码变更可以在项目分支new-objects branch
这里看到。
5. 使用构建工具
当你需要发生产环境的时候,要使用构建工具。使用很简单,效果很显著。
development build
是在提醒你, react
开发者工具在开发环境使用的
如果你是用 create-react-app
来构建你的应用的,可以直接执行命令 npm run build
。
这样会打包出适合线上环境的优化后的文件。
6. 使用代码分割
当你打包你的应用的时候,你很可能会得到一个很大的文件。
随着你应用的复杂程度的增加, bundle 也会越来越大。
一旦用户访问网页,就会接收到整个 app
的全部代码。
代码分割是指,将代码分成一部分一部分,当用户需要的时候,再去请求,而不是一下子全部返回给用户。
最常见的例子就是路由的代码分割。在这种方式下,应用会根据路由变化,返回对应的代码片段。/home
会看的一部分代码,/about
会看到一部分。
另一个应用场景就是基于组件的代码分割。在这种情况下,给用户展示的就不是一个组件,而是延迟将组件发送给用户。
这里最重要的是理解和权衡应用功能和用户体验之间的关系,采用用哪种方式并不重要。
代码分割的方式是可以提供你的应用的性能的。
这里我就不展开来说了,如果你对代码分割感兴趣,你可以参考官方文档 React docs.
。他们给出的解释更专业。
结论
现在你有了解决 react
应用的性能问题,常用的跟踪和修复问题的方法,快去试试让你的App
体验更快吧!
以上是 一个用于解决 React 常见问题的 Checklist 的全部内容, 来源链接: utcz.com/a/68740.html