-
Notifications
You must be signed in to change notification settings - Fork 0
Description
本文不单是针对性能的研究,也应作为开发高效 React 应用过程中应当考虑的细节。
React 框架本身很快,但一些不推荐的做法还是会很容易造成无意义的性能损耗。
为什么 React 会这么快
React 中,对于每个 dom 节点,都使用一个 plain object 来表示,称为一个 vnode (虚拟节点),而各个 vnode 也就组成一个 vtree(虚拟树),这个虚拟树也就对应着整个实际页面的 Dom 结构。在使用中,对节点内容的更改即为对该 vnode 的更改,更改后会进行新 vtree 与旧 vtree 的差异计算(diff 计算),找到最小的更改(一个 patch),将它应用到实际 Dom 的更新中,这就比直接操作真实节点快很多。
不只是 React,很多其他的前端框架(比如 Vue)也都采用了 virtual dom 的做法。
看一下大概的更新流程:
组件的 props/state 更改 => 组件的 shouldComponentUpdate => vtree 计算最小更改 => 更新实际 Dom
其中,仅有前两步受开发者控制。
什么原因会导致拖慢性能
在我们的开发过程中,经常会为了方便,将一整个对象传递给组件,即使只需要其中的一个属性。比如一个展示用户姓名的组件,传递对象 user = { name: 'a', time: 111 } 给组件的话,一旦其他属性(例如 time)更新,该组件也会触发 render 进行相应的更新,但更新前后的组件看起来并没有什么区别。
这就是无意义的更新,可能是多余的 props 更新,也可能是 state 的更新导致。需要注意的是,一旦父组件进行多余的更新,很可能其所有的子组件同样会进行更新,也就造成了更多的资源浪费。
同时,若组件的 render 方法带有太多的逻辑判断或方法参数定义,每次执行该方法都耗费时间的话,整个应用的渲染也就会变得很慢。即使是必要的更新,也导致了性能被拖慢。
可知,要提升 React 的性能有两个方向:减少 render 次数,加快 render 执行速度。
什么原因会导致 re-render
在组件第一次渲染的时候,会执行 render 方法渲染组件。当组件的 props/state 更新时,不管 props 是否为传递给组件的属性或 state 是否传递给子组件,默认都会使 shouldComponentUpdate 方法返回 true ,使得 render 方法重新执行,即 re-render。
简化 render 方法的逻辑
render 方法应尽量简洁易懂,保持纯粹性,代码越少越好。对于定义变量之类的,若可通过 props / state 获得,可采用计算属性的形式定义:
class App extends React.Component {
get count () {
return this.props.todos.length
}
render() {
return <div>待办事件数量: {this.count}</div>
}
}特别注意不要在 render 的子组件中,用箭头函数绑定执行方法,最好是在组件的声明中直接构造属性方法。以下是错误例子:
class App extends React.Component {
render() {
return <button onClick={() => alert('点击')}>点击</button>
}
}因为在 render 中,每次声明的函数都是不同个的,导致 render 被更新。
拆分容器型组件与展示型组件
为了简化 render 函数,保持 render 的纯粹,很大一部分工作应当放在规划组件的分工上。按 React 的推荐,将组件划分为容器型组件与展示型组件。
容器型组件负责数据逻辑处理相关的操作,例如网络请求或与 redux 结合对全局数据的下发,这类组件不会过多关注具体样式如何呈现,主要负责对处理完后的数据进行选择性地组件渲染,即何时渲染?渲染什么?这类组件一般带有局部状态,用于交互。
展示型组件单纯按接受到的数据进行实际的样式渲染,大多为方法型组件(Function Component) 或纯组件(Pure Component)。这类组件一般不带有局部状态,在确定输入的情况下有明确的输出。
在进行组件拆分后,便能针对性地对组件的渲染策略进行优化,特别是对展示型组件,明确所需输入参数后可大大减少该组件的渲染复杂度,减少渲染计算耗时与次数。
减少 props 不必要的更改
经常会有人用一个复杂的对象作为 props 传入组件,需要用时才使用其中一小部分属性,这就导致该对象的其他属性变动时,该组件仍然可能被进行不必要的更新,而且对于明确该组件的实际意义也带来了不少的阻碍。
组件需要什么值就传入什么值,不要塞一堆没意义的内容,增加了计算渲染的复杂度。
关于列表项的 key 值选择
在 vtree 的差异计算中,会根据 vnode 的 key 值来判断是否复用旧的节点资源。
对于一个列表(可变对象)来说,列表项的内容修改与数量增删是很常见的操作,如果不按规范设置 key 值(比如有些人会贪图方便,用该项的索引作为 key 值),可能会导致一旦列表内容新增或减少,即使所渲染的项内容不变,也可能会导致输入参数不一致而引发重渲染。
索引的改变导致在差异计算中被视为不同一个节点内容,而重新进行节点的创建与销毁。
所以推荐使用与内容更加相关的参数作为 key 值,例如使用内容相关的 hash 。
补充一点,关于列表中的 key 值,有助于差异计算中更快更准确的查找对应的 vnode;且设置了内容无关的 key 值很可能导致节点被直接复用而产生副作用(状态残留之类的)。
简化 state
同理,除了 props,state 也需要进行必要的简化。用一个复杂的对象作为 state 也会带来上述的问题。同时,不要频繁使用 this.setState 来更新 state,无意义的更新也会带来反复渲染,拖慢性能。
将 store 扁平化
数据源除了 props 与 state,还有用于存放全局状态数据的 store。在设计 store 的结构时,应当尽量扁平化,不应有过深的层次关系,这样在使用 redux 的 mapStateToProps 等操作时,不至于传入一个过于复杂的对象,导致别的组件更新全局状态,该组件也被动进行了无意义的更新。
大招:使用 shouldComponentUpdate 控制是否 re-render
在一些特殊的组件中,可能会有 props 或 state 更新,但不必要重新渲染的情况,即需自定义渲染策略。此时就该使用优化大招:shouldComponentUpdate。
shouldComponentUpdate 是组件的一个内部方法,在每次 props 与 state 更新时,会判断该方法的返回值,若为 True 则进行更新。默认该方法一直返回 True。
所以要进行自定义更新策略,可以通过修改该方法进行解决:
class App extends React.Component {
shouldComponentUpdate (nextProps, nextState) {
# nextProps 与 nextState 表示即将更新的 props 与 state。而 this.props 与 this.state 表示旧的值
if (nextProps.path === this.props.path) {
# 当 path 不变时不重新渲染
return false
} else {
return true
}
}
render() { ... }
}PureComponent 的使用
有时候我们的 props 与 state 指向的是一个简单对象,而当他们被重新赋值时,即使内容字段一致,也会被视为不同对象,此时我们可能需要使用 shouldComponentUpdate 判断各个属性是否不同,来控制不必要的重渲染。
还好,React 已经提供了这种机制,即纯组件,PureComponent。纯组件没做什么其他的事,仅仅帮你写好了 shouldComponentUpdate 中 props 与 state 的浅比较。注意,它只比较一层的值,所以还是请先简化 props 与 state。
使用:
class App extends React.PureComponent {
# 直接继承 React.PureComponent 即可
render() { return <div>{this.props.path}</div> }
}PureComponent vs FunctionComponent
React 官方推崇的还是方法型组件(也称为无状态组件 stateless compoennt):
# 方法型组件就一个纯函数
const item = ({path}) => <div>{path}</div>
class App extends React.Component {
render() {
# 使用起来跟其他组件没什么区别
<item path='aaa' />
}
}这种组件没有生命周期与ref之类的,减少了不必要的内存申请,在通过 babel 转码后代码量也比一般组件少很多。
当然这也就带来了一些小小的缺点,没有 shouldComponentUpdate 来手动控制渲染策略,完全依靠父组件传过来的值来判断。同时,一旦父组件重新渲染,该组件必将一起重新渲染。
所以方法型组件还是比较适合一些很小的内容,这样重新渲染的开销也小。
而纯组件的缺点,则是随着组件内容的增大,在shouldComponentUpdate 中一旦判断为 True,就意味着会进行两次重复的 diff 判断(vtree 中也会进行一次 diff 判断),这样就增大了计算的开销。
所以纯组件还是适合比方法型组件稍大一点的内容渲染,同时需保证 props 与 state 的复杂度应该尽量低。