【譯】React應用性能優化

這段時間對本身寫的React應用的性能作了一些分析以及優化,發現項目中產生性能問題的緣由主要來自兩個方面:html

  1. 大量的數據渲染使組件進行沒必要要的diff過程,致使應用卡頓;react

  2. 部分交互操做頻繁的組件中使用了一些沒必要要的DOM操做,以及在處理好比scroll事件,resize事件等這類容易致使瀏覽器不停從新渲染的操做時,混雜了大量的計算以及混亂的DOM操做,致使瀏覽器卡頓。git

今天主要想討論的是關於第一點的優化,至於第二點,這並非React爲咱們帶來的問題,而是咱們對瀏覽器的渲染機制理解和思考還有所欠缺的表現,下次咱們再去深刻的探討這個問題。github

這篇文章是我在探查緣由的時候,在Medium上看到的。原文地址:Performance optimisations for React applications算法

先聲明,做者的觀點並非徹底可取的,他在文章中的闡述是基於React與Redux的,但事實上他並無徹底使用Redux的connect()函數,這一點Dan也在Tweet上指出了。不過即便這樣,對咱們單純的使用React來講,也是頗有意義的。針對Dan的觀點,做者也在文章的評論中進行了補充。redux

TL;DR;
React應用主要的的性能瓶頸來自於一些冗餘的程序處理以及組件中的DOM diff的過程。爲了不這種狀況,在你的應用中儘量多的讓shouldComponentUpdate返回false瀏覽器

而且你還要加快這個過程:數據結構

  1. shouldComponentUpdate中的條件判斷應該儘量的快app

  2. shouldComponentUpdate中的條件判斷要儘量的簡單ide

如下是正文


是什麼形成了React應用的性能瓶頸?

  1. 不須要更新DOM的冗餘處理

  2. 對大量不須要更新的DOM節點進行diff計算(雖然Diff算法是使React應用表現良好的關鍵,但這些計算並不可以徹底忽略不計)

React中默認的渲染方式是什麼?

讓我來研究下React是如何渲染一個組件的

首次render

對於首次渲染來講,全部的節點都應當被渲染(綠色的表示被渲染的節點)

圖中的每一個節點都被渲染了,咱們的應用目前處於初始狀態。

發起改變

咱們想要更新一段數據,而跟這個數據相關的只有一個節點。

理想中的更新

咱們只想渲染到達葉子節點的關鍵路徑。

默認的渲染行爲

若是咱們什麼都不作的話,React默認會這樣作:(orange = waste)

全部的節點都被渲染了。

每一個React組件中都有一個shouldComponentUpdate(nextProps, nextState)方法。它返回一個Bool值,當組件應當被渲染時返回true,不該當被渲染時返回false。當return false時,組件中的render方法壓根不會被執行。然而在React中,即使你沒有明確的定義shouldComponentUpdate方法,shouldComponentUpdate仍是會默認返回True。

// default behaviour
shouldComponentUpdate(nextProps, nextState) {
    return true;
}

這意味着,當咱們對默認行爲不作任何修改時,每次改動頂層的props,整個應用的全部組件都會執行其render方法。這就是產生性能問題的緣由。


那麼咱們如何實現理想化的更新操做呢?

在你的應用中儘量多的讓shouldComponentUpdate返回false

而且你還要加快這個過程:

  1. shouldComponentUpdate中的條件判斷應該儘量的快

  2. shouldComponentUpdate中的條件判斷要儘量的簡單

加快shouldComponentUpdate中的條件判斷

理想化的狀況中,咱們不該該在shouldComponentUpdate函數中進行深度比較,由於深度比較是比較消耗資源的一件事,特別是咱們的數據結構嵌套特別深,數據量特別大的時候。

class Item extends React.Component {
    shouldComponentUpdate(nextProps) {
      // expensive!
      return isDeepEqual(this.props, nextProps);
    }
    // ...
}

一個可供替代的方法是在一個數據發生改變時,修改對象的引用而不是它的值。

const newValue = {
    ...oldValue
    // any modifications you want to do
};


// fast check - only need to check references
newValue === oldValue; // false

// you can also use the Object.assign syntax if you prefer
const newValue2 = Object.assign({}, oldValue);

newValue2 === oldValue; // false

能夠把這個技巧用在Redux中的reducer中:

// in this Redux reducer we are going to change the description of an item
export default (state, action) => {

    if(action.type === 'ITEM_DESCRIPTION_UPDATE') {

        const {itemId, description} = action;

        const items = state.items.map(item => {
            // action is not relevant to this item - we can return the old item unmodified
            if(item.id !== itemId) {
              return item;
            }

            // we want to change this item
            // this will keep the 'value' of the old item but 
            // return a new object with an updated description
            return {
              ...item,
              description
            };
        });

        return {
          ...state,
          items
        };
    }

    return state;
}

若是你採用了這種方式,那麼在你的shouldComponentUpdate方法中只須要檢查對象的引用就能夠。

// super fast - all you are doing is checking references!
shouldComponentUpdate(nextProps) {
    return !isObjectEqual(this.props, nextProps);
}

下面是isObjectEqual函數的一種簡易實現:

const isObjectEqual = (obj1, obj2) => {
    if(!isObject(obj1) || !isObject(obj2)) {
        return false;
    }

    // are the references the same?
    if (obj1 === obj2) {
       return true;
    }

   // does it contain objects with the same keys?
   const item1Keys = Object.keys(obj1).sort();
   const item2Keys = Object.keys(obj2).sort();

   if (!isArrayEqual(item1Keys, item2Keys)) {
        return false;
   }

   // does every object in props have the same reference?
   return item2Keys.every(key => {
       const value = obj1[key];
       const nextValue = obj2[key];

       if (value === nextValue) {
           return true;
       }

       // special case for arrays - check one level deep
       return Array.isArray(value) &&
           Array.isArray(nextValue) &&
           isArrayEqual(value, nextValue);
   });
};

const isArrayEqual = (array1 = [], array2 = []) => {
    if (array1 === array2) {
        return true;
    }

    // check one level deep
    return array1.length === array2.length &&
        array1.every((item, index) => item === array2[index]);
};

讓shouldComponentUpdate中的條件判斷更簡單

下面是一個比較複雜的shouldComponentUpdate函數:

// Data structure with good separation of concerns (normalised data)
const state = {
    items: [
        {
            id: 5,
            description: 'some really cool item'
        }
    ],

    // an object to represent the users interaction with the system
    interaction: {
        selectedId: 5
    }
};

這樣的數據結構讓你的shouldComponentUpdate函數變得複雜:

import React, {Component, PropTypes} from 'react';

class List extends Component {

    propTypes = {
        items: PropTypes.array.isRequired,
        interaction: PropTypes.object.isRequired
    }

    shouldComponentUpdate (nextProps) {
        // have any of the items changed?
        if(!isArrayEqual(this.props.items, nextProps.items)){
            return true;
        }
        // everything from here is horrible.

        // if interaction has not changed at all then when can return false (yay!)
        if(isObjectEqual(this.props.interaction, nextProps.interaction)){
            return false;
        }

        // at this point we know:
        //      1. the items have not changed
        //      2. the interaction has changed
        // we need to find out if the interaction change was relevant for us

        const wasItemSelected = this.props.items.any(item => {
            return item.id === this.props.interaction.selectedId
        });
        const isItemSelected = nextProps.items.any(item => {
            return item.id === nextProps.interaction.selectedId
        });

        // return true when something has changed
        // return false when nothing has changed
        return wasItemSelected !== isItemSelected;
    }

    render() {
        <div>
            {this.props.items.map(item => {
                const isSelected = this.props.interaction.selectedId === item.id;
                return (<Item item={item} isSelected={isSelected} />);
            })}
        </div>
    }
}

問題1:龐大的shouldComponentUpdate函數

從上面的例子就能夠看出來,即使是那麼一小段很簡單的數據結構,shouldConponentUpdate函數依然有如此繁雜的處理。這是由於這個函數須要瞭解數據結構,以及每一個數據之間又怎樣的關聯。因此說,shouldComponentUpdate函數的大小和複雜度,是由數據結構決定的。

這很容易引發兩個錯誤:

  1. 不應返回false時,返回了false(狀態在程序中沒有被正確處理,致使視圖不更新)

  2. 不應返回true時,返回了true(視圖每次都更新,引發了性能問題)

何須爲難本身呢?你想要讓你的程序足夠簡單,而不須要仔細考慮這些數據之間的關係。(因此,想要讓程序變得簡單,必定要設計好數據結構)

問題2:高度耦合的父子組件

應用廣泛都是耦合度越低越好(組件之間要儘量的不互相依賴)。父組件不該該試圖去理解子組件是如何運行的。這容許你修改子組件的行爲,而父組件不須要知道更改(假定子組件的PropTypes不變)。這一樣意味着子組件能夠獨立運行,而不須要父組件嚴格的控制它的行爲。

規範化你的數據結構

經過規範化你的數據結構,你能夠很方便的只經過判斷引用是否更改來判斷視圖是否須要更新。

const state = {
    items: [
        {
            id: 5,
            description: 'some really cool item',

            // interaction now lives on the item itself
            interaction: {
                isSelected: true
            }
        }
    ]
};

這樣的數據結構讓你在shouldComponentUpdate函數中的更新檢測更加簡單。

import React, {Component, PropTypes} from 'react';

class List extends Component {

    propTypes = {
        items: PropTypes.array.isRequired
    }

    shouldComponentUpdate (nextProps) {
        // so easy
        return isObjectEqual(this.props, nextProps);
    }

    render() {
            <div>
                {this.props.items.map(item => {

                    return (
                    <Item item={item}
                        isSelected={item.interaction.isSelected} />);
                })}
            </div>
        }
}

若是你想要更新其中的一個數據,好比interaction,你只須要更新整個對象的引用就能夠了。

// redux reducer
export default (state, action) => {

    if(action.type === 'ITEM_SELECT') {

        const {itemId} = action;

        const items = state.items.map(item => {
            if(item.id !== itemId) {
                return item;
            }

            // changing the reference to the whole object
            return {
                ...item,
                interaction: {
                    isSelected: true
                }
            }

        });

        return {
          ...state,
          items
        };
    }

    return state;
};

引用檢查和動態props

先看一個建立動態props的例子

class Foo extends React.Component {
    render() {
        const {items} = this.props;

        // this object will have a new reference every time
        const newData = { hello: 'world' };


        return <Item name={name} data={newData} />
    }
}

class Item extends React.Component {

    // this will always return true as `data` will have a new reference every time
    // even if the objects have the same value
    shouldComponentUpdate(nextProps) {
        return isObjectEqual(this.props, nextProps);
    }
}

一般咱們不在組件中建立新的props,只是將它傳遞下去。
然而下面這種內部循環的方式卻愈來愈廣泛了:

class List extends React.Component {
    render() {
        const {items} = this.props;

        <div>
            {items.map((item, index) => {
                 // this object will have a new reference every time
                const newData = {
                    hello: 'world',
                    isFirst: index === 0
                };


                return <Item name={name} data={newData} />
            })}
        </div>
    }
}

這是在建立函數中常用的。

import myActionCreator from './my-action-creator';

class List extends React.Component {
    render() {
        const {items, dispatch} = this.props;

        <div>
            {items.map(item => {
                 // this function will have a new reference every time
                const callback = () => {
                    dispatch(myActionCreator(item));
                }

                return <Item name={name} onUpdate={callback} />
            })}
        </div>
    }
}

解決這個問題的策略

  1. 避免在組件內部建立動態props(改善你的數據結構,使props能夠被直接用來傳遞)

  2. 將動態props當作知足===不等式的類型傳遞(eg: Bool, Number, String)

const bool1 = true;
const bool2 = true;

bool1 === bool2; // true

const string1 = 'hello';
const string2 = 'hello';

string1 === string2; // true

若是你真的須要傳遞一個動態對象,你能夠傳遞一個對象的字符串表示,而且這個字符串應當能夠在子組件中從新解讀爲相應的對象。

render() {
    const {items} = this.props;

    <div>
        {items.map(item => {
            // will have a new reference every time
            const bad = {
                id: item.id,
                type: item.type
            };

            // equal values will satify strict equality '==='
            const good = `${item.id}::${item.type}`;

            return <Item identifier={good} />
        })}
    </div>
}

特例:函數

  1. 儘可能不要傳遞函數。在子組件須要時纔去觸發相應的actions。這樣作還有一個好處是將業務邏輯與組件分離開來。

  2. 忽略shouldComponentUpdate函數中對functions的檢查,由於咱們沒法知曉函數的值是否發生改變。

  3. 建立一個不可變數據與函數的映射。你能夠在執行componentWillReveiveProps函數時,把這個映射放到state中。這樣的話每次render時將不會獲得一個新的引用,便於執行在shouldComponentUpdate時的引用檢查。這個方法比較麻煩,由於須要維護和更新函數列表。

  4. 建立一個有正確this綁定的中間組件。這樣也並不理想,由於在組件的層次結構中引入了冗餘層。(實際上做者的意思是將函數的定義從render函數中移出,這樣每次的render就不會建立新的引用了)

  5. 避免每次執行render函數時,都建立一個新的函數。

關於第四點的例子:

// introduce another layer 'ListItem'
<List>
    <ListItem> // you can create the correct this bindings in here
        <Item />
    </ListItem>
</List>

class ListItem extends React.Component {

    // this will always have the correct this binding as it is tied to the instance
    // thanks es7!
    const callback = () => {
          dispatch(doSomething(item));
    }

    render() {
        return <Item callback={this.callback} item={this.props.item} />
    }
}

工具

上面列出的全部規則和技術都是經過使用性能測量工具發現的。 使用工具將幫助你找到應用程序中特定的性能問題。

console.time

這個工具至關簡單。

  1. 開始計時

  2. 程序運行

  3. 結束計時

一個很棒的方式是用Redux的中間件來測試性能。

export default store => next => action => {
    console.time(action.type);

    // `next` is a function that takes an 'action' and sends it through to the 'reducers'
    // this will result in a re-render of your application
    const result = next(action);

    // how long did the render take?
    console.timeEnd(action.type);

    return result;
};

用這個方法,你能夠記錄每一個操做及其在應用程序中渲染所花費的時間。 你能夠快速查看是哪些操做須要耗費不少時間來執行,這給咱們提供瞭解決性能問題的一個起點。 有了這個時間值,還有助於咱們查看咱們對代碼的更改對應用程序產生的影響。

React.perf

這個工具跟console.time用起來很像,可是它是專門用來檢測React應用性能的。

  1. Perf.start

  2. 程序運行

  3. Perf.stop

依然是用Redux的中間件舉個例子

import Perf from 'react-addons-perf';

export default store => next => action => {
    const key = `performance:${action.type}`;
    Perf.start();

    // will re-render the application with new state
    const result = next(action);
    Perf.stop();

    console.group(key);
    console.info('wasted');
    Perf.printWasted();
    // any other Perf measurements you are interested in

    console.groupEnd(key);
    return result;
};

與console.time方法相似,您能夠查看每一個操做的表現數據。 有關React性能插件的更多信息,請參閱此處

瀏覽器開發者工具

CPU分析器的Flame圖也有助於在應用程序中查找性能問題。

Flame圖顯示性能展現文件中每毫秒代碼的JavaScript堆棧的狀態。 這給你一個方法來確切地知道哪一個函數在記錄期間的哪一個點執行,運行了多長時間,以及是從哪裏被調用的 - Mozilla

Firefox: see here
Chrome: see here


感謝閱讀以及一切能讓React應用性能提升的方式!


做者的補充:

在檢查每一個子組件的列表組件上使用shouldComponentUpdate(),並非很是有用。

當你有不少大列表的時候,這個方法是頗有用的。可以徹底跳過列表的從新渲染時一個巨大的勝利。可是若是你的應用中只有一個大列表,那麼這樣作其實沒有任何效果,由於你的任何操做都是基於這個列表的,意味着列表中的數據確定會有所改變,那麼你徹底能夠跳過對更新條件的檢查。

相關文章
相關標籤/搜索