React性能优化(译)

原文请见:https://facebook.github.io/react/docs/optimizing-performance.html

React内置了很多聪明的方法来减少用于更新UI的耗时DOM操作。对大多数应用来说,不用做太多具体的优化工作就可以创造出快速的用户界面。然而,还是有很多方法去加速我们的React应用。

使用生产模式构建

如果你正测试或体验你的应用性能问题,确保你在最小生产模式构建环境下进行:

  • 对于使用create-react-app的用户,可以运行npm run build并按照指示操作
  • 对于单个网页,提供了.min.js版本
  • 对于Browserify,配合参数NODE_ENV=production运行
  • 对于Webpack,将下面插件加到生产配置文件中:
    1
    2
    3
    4
    5
    6
    new webpack.DefinePlugin({
    'process.env': {
    NODE_ENV: JSON.stringify('production')
    }
    }),
    new webpack.optimize.UglifyJsPlugin()

开发模式构建包含了额外有用的警告信息,但是由于其需要额外记录一些信息会导致应用变慢。

避免Reconciliation(调解)

React构建并维护了一套UI内部表现机制。包括组件返回的React元素。使得React可以避免在非必要时创建和访问DOM节点,因为对他们的操作往往比操作Javascript对象要慢。这种机制被称作”virtual DOM”,不过它在React Native中同样适用。

当一个组件的props或state发生改变时,React会通过比较最新的返回元素和之前已经渲染的元素来判断是否有必要更新真实的DOM节点。当他们不相等时,React会更新DOM。

有些情况下,你的组件可以通过重写生命周期函数shouldComponentUpdate(组件重新渲染前触发)来进行加速,
默认该函数返回true,表示应该更新组件。

1
2
3
shouldComponentUpdate(nextProps, nextState) {
return true;
}

如果你知道什么情况下你的组件不需要更新,你可以在shouldComponentUpdate中返回false,跳过组件的重新渲染阶段,包括调用render()方法以及其以后的其他方法。

shouldComponentUpdate 实战

下图是一个组件树。SCU表示shouldComponentUpdate的返回值,vDOMEq表示前后两次虚拟DOM是否相同,圆圈颜色表示组件是否需要更新。
should-component-update

介于C2的shouldComponentUpdate返回false,React不会尝试重新渲染C2,甚至不会调用C4和C5的shouldComponentUpdate方法。

C1和C3的shouldComponentUpdate返回true,所以React必需往下继续检查。C6的shouldComponentUpdate返回true,并且前后两次虚拟DOM不相同,所以需要更新真实DOM。

有意思的是C8。虽然shouldComponentUpdate返回true,但是因为比较虚拟DOM发现没有变化,所以该组件不更新。

注意到React只需要更新C6,当然,这是无法避免的。但是通过比较虚拟DOM,C8被排除了。对于C2和其子组件,以及C7来说,根本不需要比较虚拟DOM,因为在shouldComponentUpdate就排除了,连render都无需调用。

例子

如果你的组件更新的唯一条件是当props.colorstate.count发生了变化,你可以在shouldComponentUpdate中进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}

shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}

render() {
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
}
}

如果你的组件变得更加复杂了,你可以在所有的propsstate中使用“浅比较”来决定组件是否需要更新。你可以通过继承React.PureComponent来让React自动为你做这个事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}

render() {
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
}
}

不过,当数据不是基本类型时,这个会有问题,看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}

class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// This section is bad style and causes a bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}

render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}

问题出在PureComponent只对新旧props进行了浅对比,而ListOfWords生命周期函数shouldComponentUpdate中的newProps.wordsthis.props.words指向的是同一个对象的引用,自然他们就相等了,为了验证,我加了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
class ListOfWords extends React.PureComponent {

shouldComponentUpdate(newProps, newState) {
console.log(this.props.words);
console.log(newProps.words);
console.log(this.props.words === newProps.words);
return true;
}
render() {
return <div>{this.props.words.join(',')}</div>;
}
}

结果返回:

1
2
3
["marklar","marklar"]
["marklar","marklar"]
true

不可变数据的力量

一个简单的解决该问题的方法是避免使用可变数据,例如:上面的handleClick可以改写成这样:

1
2
3
4
5
handleClick() {
this.setState(prevState => ({
words: prevState.words.concat(['marklar'])
}));
}

或者使用扩展运算符…

1
2
3
4
5
handleClick() {
this.setState(prevState => ({
words: [...prevState.words, 'marklar'],
}));
};

如果是对象呢?比如要改变一个对象的某个属性,原来的写法是这样:

1
2
3
function updateColorMap(colormap) {
colormap.right = 'blue';
}

现在可以改写成这样:

1
2
3
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}

上面的函数返回了一个全新的对象,而不是修改原来的对象

或者用ES6语法,像这样:

1
2
3
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}

测试结果返回:

1
2
3
["marklar"]
["marklar","marklar"]
false

使用不可变数据结构

Immutable.js是另外一个解决上述问题的方法。具体详情请见官网,本文就不赘述了。

参考