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

十6、性能优化

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

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

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

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

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

  • 对于Create React App来讲,你须要在构建时运行npm run buildweb

  • 对于单文件来讲,咱们提供了生产环境版本.min.jsnpm

  • 使用的是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”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是另外一种解决这个问题的方法。 它提供不可变的,持久的集合,经过结构共享工做:

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

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

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

相关文章
相关标签/搜索