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是另外一種解決這個問題的方法。 它提供不可變的,持久的集合,經過結構共享工做:

  • 不可變:一旦建立,集合不能在另外一個時間點更改。

  • 持久性:能夠從先前的集合和類集合的突變中建立處一個新集合。 建立新集合後,原始集合仍然有效。

  • 結構共享:使用盡量多的與原始集合相同的結構建立新集合,從而將最低程度的減小複製來提升性能。

相關文章
相關標籤/搜索