React 性能优化

虽然说使用 React 已经可以得到很好的性能了,但是还是会有一些使用方法的不对会导致 React 应用的性能下降。官方文档也有一篇文章专门介绍如何优化性能,「Avoid Reconciliation」这一节我觉得值得一看。

下面我要说的也是围绕「Avoid Reconciliation」这一节的内容来进行展开。首先「Reconciliation」是什么意思呢,我理解的是需要调用组件的 render() 方法进行重新渲染,用生成新的虚拟 DOM 与老的虚拟 DOM 对比,以此来决定是否需要更新 DOM。React 在重新渲染之前会去判断 shouldComponentUpdate() 函数返回的值。如果为 true 就是需要重新渲染,如果为 false 就什么也不做。shouldComponentUpdate() 默认返回 true,也就是始终需要重新渲染。所以我们需要重新实现 shouldComponentUpdate() 只有在我们需要重新渲染的时候才返回 true。这样一来是不是每个组件都需要我们去实现 shouldComponentUpdate(),实际上是不用。如果你是通过继承 PureComponent 来实现的组件,只有在 state 或者 props 改变的时候才会需要重新渲染,可以看下 ReactCompositeComponent.updateComponent() 源码。建议多使用 PureComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
//……
var shouldUpdate = true;
if (inst.shouldComponentUpdate) {
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
} else {
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
}
}
//……
}

我们可以想象一下在一个表单页面。有很多的 input 组件。每输入一个值都会导致所有的 input 重新渲染。为了优化,我们需要封装一下 input 组件。在只有自身改变的时候才重新渲染。如果页面上有10个 input,那么优化过后就只用花之前的 1/10 的性能就可以完成之前的渲染任务。这样就指导我们一个组件不能写的太大,做好组件间的隔离,频繁改动的组件要不影响其他组件。

之前自己一直有个错误的想法,认为 PureComponent 实现的 shouldComponentUpdate() 方法里面 shallowEqual(prevProps, nextProps) 不能正确的判断 function 导致 props 里面如果传了 function 的话,即使所有属性都没改变,也会引起组件的重新渲染。当时还把这个当成教条牢记在心中,props 里面有 function 的话不能使用 PureComponent,因为这会导致 shouldComponentUpdate() 不能像预期一样正常工作。直到写这篇文章的时候再去看以前的代码,才发现,原来我传入的 function 是一个柯里化函数,每次父组件 render() 都会生成一个新的函数。好囧。

为了避免我们无意识的在 render() 里面创建了一个新对象。就要避免对产生新对象的方法的使用。尤其是需要传递给子组件的 props,这里有一个比 render() 是纯函数还要高的要求。不只要求同样的传入始终返回一样的结果,还要求如果输入没有变化的话,那么就不应该输入新的对象。换句话说不产生新对象就对了。包括自身的 propsstate。如果你是使用的 Redux 那么就应该在 reducer 里面就把所有数据准备好。

说了这么多,搞得我们像走钢丝样都不敢轻易写代码了。有没有比较好的方式去检查不应该重新渲染的地方却重新渲染了呢?既然我们能想到这个问题当然那些大牛也能想到,并且早就提供了成熟的类库供我们使用。这个类库就 是 why-did-you-update。一旦我们在项目中引入了 why-did-you-update,就会在控制台中以警告的形式提示我们哪些地方该优化了。

why-did-you-update 触发提示的条件是,如果 shouldComponentUpdate() 返回的是 true, 那么组件的生命周期方法 componentDidUpdate() 调用时, why-did-you-update 就会去比较每个 props 的属性,每个 state 的属性,去判断到底是不是真的发生了变化。如果你是骗了 React 去更新页面,那么就不会逃脱 why-did-you-update 的法眼,不管是浅拷贝还是深拷贝。但是如果触发页面重新请求数据,但是数据还是以前的数据,没有更新,那么最好有一种缓存机制直接返回之前的数据。但是也不要忘了,一旦有数据变化一定要返回新的对象。

虚拟 DOM 内部是如何工作的

Optimizing Performance

React PureComponent 源码解析

[译]高性能React: 3个工具提高你app的速度