React从入门到精通系列之(16)性能优化

88 阅读7分钟
原文链接: segmentfault.com

十六、性能优化

在React内部,React使用了几种比较聪明的技术来实现最小化更新UI所需的昂贵的DOM操作的数量。

对于许多应用来说,使用React将很快速的渲染出用户界面,从而无需进行大量工作来专门做优化性能的工作。

大概有以下有几种方法来加快你的React应用程序。

使用生产环境的配置进行构建

如果你在React应用中进行基准测试或这遇到了性能问题,请首先确保你是使用的压缩后线上版本js文件来进行的测试:

  • 对于Create React App来说,你需要在构建时运行npm run build

  • 对于单文件来说,我们提供了生产环境版本.min.js

  • 使用的是Browserify,你先设置NODE_ENV=production然后运行。

  • 使用的是webpack,你需要在生产环境配置中加入以下插件:

new webpack.DefinePlugin({
    'process.env': {
        NODE_ENV: JSON.stringify('production')
    }
}),
new webpack.optimize.UglifyJSPlugin();

在构建应用程序时开发构建工具可以打印一些有帮助的额外警告。
但是由于需要额外地记录这些警告信息,所以它也会变得更慢。

避免重复处理DOM

React会创建并维护所渲染的UI内部表示信息。其中包括从组件返回的React元素。 此表示信息使React避免创建DOM节点和访问那些没有必要的节点,因为这样做可能会比JavaScript对象上的一些操作更慢。 有时它被称为“虚拟DOM”

当组件的propsstate更改时,React通过将最新返回的元素与先前渲染的元素进行比较来决定是否需要实际的DOM更新。 当它们不相等时,React将更新DOM。

在某些情况下,您的组件可以通过重写生命周期函数shouldComponentUpdate来加快所有这些操作。这个函数会在重新渲染之前触发。 此函数的默认实现返回true,让React执行更新:

shouldComponentUpdate(nextProps, nextState) {
    return true;
}

如果你知道在某些情况下你的组件不需要更新,你可以从shouldComponentUpdate中返回false,而不是跳过整个渲染过程,其中包括调用当前组件和下面的render()

shouldComponentUpdate的应用

这里是一个组件的子树。 对于其中每一个子树来说,SCU指示shouldComponentUpdate返回什么,vDOMEq指示渲染的React元素是否相等。 最后,圆圈的颜色表示组件是否必须重新处理。

虚拟DOM比较

因为shouldComponentUpdate对于以C2为根的子树返回了false,所以React没有尝试渲染C2,因此甚至不必在C4C5上调用shouldComponentUpdate

对于C1C3shouldComponentUpdate返回true,因此React必须下到子树中并检查它们。 对于C6 子树shouldComponentUpdate返回true,并且因为渲染的元素不是相同的,React不得不更新DOM。

最后一个有趣的例子是C8。 React不得不渲染这个组件,不过由于React元素返回的元素等于之前渲染的元素,所以它不必更新DOM。

注意,React只需要做C6的DOM重新处理,这是不可避免的。
对于C8,它通过比较渲染的React元素来决定是否重新处理DOM。至于C2的子树和C7,我们在shouldComponentUpdate返回false时它甚至都不需要比较元素,并且也没有调用render()

例子

如果你的组件的唯一的改变方式就是改变props.colorstate.count,你可以用shouldComponentUpdate检查:

import React from 'react';
import ReactDOM from 'react-dom';

class CounterButton extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 1};
        this.click = this.click.bind(this);
    }

    click() {
        this.setState(prevState => ({
            count: prevState.count + 1
        }));
    }

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

    render() {
        return (
            <button color={this.props.color} onClick={this.click}>
                Count:{this.state.count}
            </button>
        );
    }
}
ReactDOM.render(
    <CounterButton color="blue"/>,
    document.getElementById('root')
);

在这段代码中,shouldComponentUpdate只是检查props.colorstate.count是否有任何变化。 如果它们的值没有更改,则组件不更新。 如果你的组件比这个例子中的组件更复杂,你可以使用类似的模式在props和state的所有字段之间做一个“浅比较”,以确定组件是否应该更新。

比较常见的模式是使用React提供的一个帮助对象来使用这个逻辑,可以直接继承React.PureComponent
所以上面这段代码有一个更简单的方法来实现同样的事情:

import React from 'react';
import ReactDOM from 'react-dom';

class CounterButton extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {count: 1};
        this.click = this.click.bind(this);
    }

    click() {
        this.setState(prevState => ({
            count: prevState.count + 1
        }));
    }

    render() {
        return (
            <button color={this.props.color} onClick={this.click}>
                Count: {this.state.count}
            </button>
        );
    }
}
ReactDOM.render(
    <CounterButton color="blue"/>,
    document.getElementById('root')
);

大多数时候,你可以使用React.PureComponent而不是编写自己的shouldComponentUpdate。 它只做一个浅层的比较,所以你不需要直接使用它,如果你的组件内部propsstate的数据有可能会突然变化,那么浅比较将失效。

浅比较的失效可能是一个更加复杂的数据结构问题(突然变化)。 例如,假设您想要一个以逗号分隔单词列表的ListOfWords组件,使用一个父WordAdder组件,当你单击一个按钮用来添加一个单词到列表中时。 下面的代码将无法正常工作:

// PureComponent在内部会帮我们对props和state进行简单对比(浅比较)
// 值类型比较值,引用类型比较引用,但是不会比较引用类型的内部数据是否改变。
// 所以就会出现一个bug,不管你怎么点button,div是不会增加的。
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: ['zhangyatao']};
        this.click = this.click.bind(this);
    }
    click() {
        // 这么写是不对的,因为state的更新是异步的,所以可能会导致一些不必要的bug
        const words = this.state.word;
        words.push('zhangyatao');
        this.setState({words: words});
    }
    render() {
        return (
            <div>
                <button onClick={this.click} />
                <ListOfWords words={this.state.words} />
            </div>
        );
    }
}

问题是PureComponent将对this.props.words的旧值和新值进行简单比较。 由于这个代码在WordAdderclick方法中改变了单词数组,所以即使数组中的实际单词已经改变,ListOfWords组件中的this.props.words的旧值和新值还是相等的。 因此即便ListOfWords具有要被渲染出来的新单词它也还是不更新任何内容。

超能力之『不会突然变化的数据』

避免此问题的最简单的方法就是避免将那些可能突然变化的数据作为你的props或state。 例如,上面的click方法里面使用concat代替push

click() {
    this.setState(prevState => ({
        count: prevState.words.concat(['zhangyatao'])
    }));
}

ES6支持数组的spread语法可以让这变得更容易。 如果您使用的是Create React App,那么此语法默认可以使用的。

click() {
    this.setState(prevState => ({
        words: [...prevState.words, 'zhangyatao']
    }));
}

您还可以把那部分有可能突然变化的数据的代码按照上面的方式给重写下,从而以避免这种问题。
例如,假设我们有一个名为colormap的对象,我们要写一个函数,将colormap.right改为'blue'。 我们可以写:

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

要将上面的代码写成不会濡染改变的对象,我们可以使用Object.assign方法:

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

updateColorMap现在会返回一个新对象,而不是改变之前的旧对象。 Object.assign在ES6中,需要polyfill

有一个JavaScript提议来添加对象spread属性,以便不会突然变化的更新对象:

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

如果使用Create React App,默认情况下Object.assign和对象spread语法都可用。

使用不突变的数据结构

Immutable.js是另一种解决这个问题的方法。 它提供不可变的,持久的集合,通过结构共享工作:

  • 不可变:一旦创建,集合不能在另一个时间点更改。

  • 持久性:可以从先前的集合和类集合的突变中创建处一个新集合。 创建新集合后,原始集合仍然有效。

  • 结构共享:使用尽可能多的与原始集合相同的结构创建新集合,从而将最低程度的减少复制来提高性能。