原文 Death by a thousand cuts - a checklist for eliminating common React performance issues
github 的地址 欢迎 star
前言(可以略过)
我们今天将会用一个具体的例子一步步的解决 React 的一些常见的性能问题。 你想知道怎样让你的 React 项目运行更快吗? 如果是,请继续往下看! 此时如果有一份常见的 React 性能优化清单是多么方便,没错,在这里就有一份!
首先,我会直接给你看项目中问题,并给出问题相应的解决方法。这样做,就和我们实际上的项目差别不大了(在一些概念上)。
这篇文章并不是长篇大论,相反地,我们讨论一些东西都是今后你们马上就能用到的。
一个示例项目
为了是文章讲的尽可能真实,我通过这个简单的 React 应用(名字叫 Cardie 的 app)带你经历各种实际的用户场景。
叫 Cardie 的手机应用 github源码地址Cardie 仅展示了用户信息,通常称呼为用户档案卡。当然,它也提供一个按钮,点击改变用户的一些信息。
点击 app 底部的按钮后,用户的信息就会被改变尽管它如此的简单,一点都不是 app,但当你通过这个例子寻找并解决性能问题之后,真实环境中 App 的性能问题你也能随之解决。 请保持耐心与放松。下面正式介绍优化清单!
1. 辨别无用浪费的渲染
辨别无用浪费的渲染,是这份清单的一个良好开端。 有几种不同的方法解决辨别问题,最简单方法,通过 React dev tools 工具实现,点击设置按钮,勾选 “highlight updates” 选项,就可以清楚看到哪些组件更新了。
当用户和 app 交互的时候,在屏幕上更新的组件就会出现绿色的闪光光罩。对于 Cardie,改变用户信息后,似乎整个 App 组件都重新渲染了
注意环绕用户卡片信息的绿色的闪光光罩。这似乎是不应该的。
当 app 运行的时候,只改变组件一小点地方,不应该导致整个组件的的重新渲染。
实际更新应该发生在 App 组件的一小部分更理想的高亮更新显示应该是这个样子:
注意现在的高亮更新只显示包含了一小块更新的区域在更复杂的应用中,无效浪费渲染的影响是巨大的!解决了重新渲染问题足够提升应用的性能了。
看完这个问题,对此你有什么解决办法?
2. 将需要频繁更新的区域抽离成独立的组件
一旦你注意到了你应用中无用的渲染,从你的组件树中拆分出频繁更新的区域是一个不错的开始。
下面具体说明如何拆分。
在 Cardie 中,App
组件通过react-redux
中 connect
函数连接到了 redux store
。从 store
中,它接受的 props
有:name
,location
,likes
以及 description
。
<App/>
需要直接从redux store
中接受的props
description
的 props 定义了当前用户的信息。
本质上,无论何时点击按钮改变了用户信息,description
的值都会改变。它的改变导致了整个 App
组件的重新渲染。
一个react组件会渲染一个由你是否想起了 React 官网中说的,每当组件的
props
或state
改变时,都会触发该组件重新渲染。
props
和 state
元素定义的组件树。如果 props
或 state
改变,这个状态组件树就会重新生成一个新的树
此时我们要把需要更新的组件单独拆分出来渲染,而不是整个 App
组件毫无意义的重新渲染。
例如,我们可以创建一个叫 Profession
组件渲染自己的DOM元素。
这样的话,Profession
组件就能渲染用户信息的description 的数据,列如I am a Coder
<Profession/>
在<App/>
中渲染
此时组件树看起来是这样:
对于profession
的props,我们关注的重点不再是<App/>
,而是变成了<Profession/>
profession
数据由<Profession/>
组件直接从redux store
获取
不管你是否使用 redux
,这时改变 profession
的 props,将不再导致 App
的重新渲染,而 <Profession/>
将重新渲染。
完成这个重构之后,你将得到理想的高亮更新显示:
想在高亮更新仅包含<Profession />
为了查看代码的更改,请从远程仓库克隆切换分支到 isolated-component branch查看
3. 适当地使用纯组件
任何提到React性能的地方都会提及到pure components
。
然而,你知道怎样在合适的时候使用纯组件吗?
当然,我们可以把每个组件都变成纯组件,但请记住不要对外层的容器组件这样操作。因为还有shouldComponentUpdate
函数。
纯组件没有自己的state,因此,纯组件的重新渲染仅仅由props
改变导致的。
一个简单的实现纯组件的方法是用React.PureComponent
代替React.Component
React.PureComponent
代替React.Component
用插画展示这个特定的用法,把Profession
拆分成粒度更小的组件。
这是拆分之前的Profession
组件
const Description = ({ description }) => {
return (
<p>
<span className="faint">I am</span> a {description}
</p>
);
}
复制代码
这是拆分之后的组件:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
复制代码
现在Description
组件就渲染4个子组件。
Description
组件有
profession
的 prop,这个 prop 只传递给了
Profession
,其它3个组件并不关心
profession
的值。
当然这些新组件的内容是极其简单的。例如,<I />
组件仅仅渲染了一个span >I </span>
元素。
有趣的是每当description
的 prop 改变之后,Profession
的每个子组件都会重新渲染。
Description
接受一个新的 prop 值,它的所有子组件都会重新渲染
我在每个子组件的render
方法添加了logs
日志,你能够确实看到每个子组件都被重新渲染了。
你可以用 react dev tools 查看高亮更新部分
这个行为是符合预期的。每当组件的 props 或者 state 改变,组件渲染的树就会重新计算。
在这个例子中,你应该认同让组件<I/>, <Am/> 以及 <A/>
重新渲染是没有任何意义的。尤其是在你的项目足够的大的时候,这将产生性能问题。
那么怎么把子组件变成纯组件呢?
针对<I/>
组件
import React, {Component, PureComponent} from "react"
//before
class I extends Component {
render() {
return <span className="faint">I </span>;
}
//after
class I extends PureComponent {
render() {
return <span className="faint">I </span>;
}
复制代码
这样修改后,当这些子组件中 prop 的值没有改变时,react 就不会重新渲染这些组件。
对于这个例子,即使你改变了 props 的值,<I/>, <Am/> 以及 <A/>
这些组件也不会重新渲染!
在重构之后观察高亮更新的显示,你会发现 <I/>, <Am/> 以及 <A/>
这些组件都没有更新,仅仅Profession
组件改变了,因为它的 prop 改变了。
<I/>, <Am/> 以及 <A/>
组件没有显示高亮更新
在大型项目中,把某些组件改成纯组件你会发现巨大的性能提升。
为了查看代码的更改,请从远程仓库克隆切换分支到 pure-component branch查看
4. 避免通过一个新的对象作为props
重复一遍,每当一个组件的 props 改变时,就会重新渲染这个组件。
当 props 或者 state 改变时,组件 tree 就会返回一个新的
如果你的组件的 props
没有改变,但 React 认为它改变了,会怎样呢?
它们也会重新渲染!
是不是困惑的?
出现这种异常情况,你需要知道 JavaScript 是怎么工作的,以及 React 是怎么对比旧的和新的 prop 值的变化的。
来看看这个例子。Description
组件的内容:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
复制代码
接下来,我们会重构 I
组件,给他传入了命名为 i
的 prop,作为一个表单提交的属性对象:
class I extends PureComponent {
render() {
return <span className="faint">{this.props.i.value} </span>;
}
}
复制代码
在 Description
组件中,以如下的方式创建了 i
并传递给 I
组件:
class Description extends Component {
render() {
const i = {
value: "i"
};
return (
<p>
<I i={i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
复制代码
接下来请耐心听我的解释,
这是完全错误的代码,但它能够正常运行,除了有一个问题。
尽管 I
组件是一个纯组件,但只要用户的职业这个属性改变了就会重新渲染 I
。
<I/>
和 <Profession/>
组件都重新渲染了。事实上,props 并没有发生改变,为什么重新渲染了呢?
为什么?
一旦 Description
组件接受了一个新的 props,就会调用 render
生成一个 React 的组件树。
在调用 render
函数之后,它就会创建一个新的 i
的实例:
const i = {
value: "i"
};
复制代码
当React执行到<I i={i} />
这里的时候,它认为i已经变了,因为它是一个新的对象(引用的对象变了),因此重新渲染了。
React判断当前的props和新的props过程的是浅比较
基本类型的数据像字符串和数字就是比较他们的值,而对象是比较它们的引用是否相等。
即使 i 的值改变前后是一样,但它指向的引用对象变了(在内存中的位置不相同),所以会重新渲染。
每次调用 render
,就会新生成一个对象。<I/>
中 prop 的i
就会被当做新的,导致重新渲染。
在更大的应用中,它就会导致无效的渲染,造成潜在的性能问题。
应该避免这样。
在应用中 prop
也会包含事件处理。
如果要避免性能浪费,那么不应这样:
...
render() {
<div onClick={() => {//do something here}}
}
...
复制代码
每次 render
都会产生一个新的函数对象。应该像这样:
...
handleClick:() ={
}
render() {
<div onClick={this.handleClick}
}
...
复制代码
明白了吗? 同理,我们如下重构<I />
:
class Description extends Component {
i = {
value: "i"
};
render() {
return (
<p>
<I i={this.i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
复制代码
现在,在 <I i={this.i} />
中 i
的引用都是 this.i
。调用 render
就不会产生新的对象。
为了查看代码的更改,请从远程仓库克隆切换分支到 new-objects branch查看
5. 使用生产模式构建打包
部署到生产环境,应该要使用 React 的生产模式。它是简单的却是最好的实践。
在开发模式下运行构建,点击 react 开发者工具就会弹出警告如果你使用了create-react-app
的脚手架,运行生产模式打包,仅需运行命令: npm run build
。 它将会尽可能优化压缩你的代码。
6. 使用代码分割(code splitting)
当你打包你的应用,你可能会把整个项目打包成一个大的块。
此时的问题,当你的应用变大的时候,那个块也会变大。
当用户访问网站,他就会获取到整个项目的一个大块代码分割提倡的不是用户立即就获取到项目的整个块,而是用户需要的时候,才动态的加载相关的项目块。
一个常见的例子就是通过路由来代码分割。这个方法,代码将会根据路由划分成不同的块。
/home
路由被划分成了一个小块,/about
路由也一样
也还有其他代码分割的方法。比如,如果一个组件当前对用户来说是不可见的,那它就可以延迟加载,当用户需要的时候在渲染。
无论你选择哪一种方法,重要的是做好权衡,不要降低了用户体验。
代码分割是极好的,它能提高你应用的性能。
我仅仅从概念上解释了代码分割的知识。如果你想知道更多代码分割的知识,请查看 React 的官方文档,很好的解释了代码分割的知识。
结尾
现在你已经获取到一份还算可以的追踪修复性能问题的清单了。快来让你的应用更快吧!
广告略过
如果有错误或者不严谨的地方,请务必给予指正,十分感谢!