【译】React运用机能优化

这段时刻对本身写的React运用的机能做了一些剖析以及优化,发明项目中发作机能题目的缘由重要来自两个方面:

  1. 大批的数据衬着使组件举行不必要的diff历程,致使运用卡顿;

  2. 部份交互操纵频仍的组件中运用了一些不必要的DOM操纵,以及在处置惩罚比方scroll事宜,resize事宜等这类轻易致使浏览器不断从新衬着的操纵时,混淆了大批的盘算以及杂沓的DOM操纵,致使浏览器卡顿。

本日重要想议论的是关于第一点的优化,至于第二点,这并非React为我们带来的题目,而是我们对浏览器的衬着机制明白和思索另有所短缺的表现,下次我们再去深切的讨论这个题目。

这篇文章是我在探查缘由的时刻,在Medium上看到的。原文地点:Performance optimisations for React applications

先声明,作者的看法并非完整可取的,他在文章中的论述是基于React与Redux的,但事实上他并没有完整运用Redux的connect()函数,这一点Dan也在Tweet上指出了。不过纵然如许,对我们纯真的运用React来讲,也是很有意义的。针对Dan的看法,作者也在文章的批评中举行了补充。

TL;DR;
React运用重要的的机能瓶颈来自于一些冗余的顺序处置惩罚以及组件中的DOM diff的历程。为了防止这类状况,在你的运用中只管多的让shouldComponentUpdate返回false

而且你还要加速这个历程:

  1. shouldComponentUpdate中的前提推断应该只管的快

  2. shouldComponentUpdate中的前提推断要只管的简朴

以下是正文

是什么造成了React运用的机能瓶颈?

  1. 不须要更新DOM的冗余处置惩罚

  2. 对大批不须要更新的DOM节点举行diff盘算(虽然Diff算法是使React运用表现优越的症结,但这些盘算并不能够完整疏忽不计)

React中默许的衬着体式格局是什么?

让我来研讨下React是如何衬着一个组件的

初次render

关于初次衬着来讲,统统的节点都应该被衬着(绿色的示意被衬着的节点)
《【译】React运用机能优化》
图中的每一个节点都被衬着了,我们的运用现在处于初始状况。

提议转变

我们想要更新一段数据,而跟这个数据相干的只要一个节点。
《【译】React运用机能优化》

抱负中的更新

我们只想衬着抵达恭弘=叶 恭弘子节点的症结途径。
《【译】React运用机能优化》

默许的衬着行动

假如我们什么都不做的话,React默许会如许做:(orange = waste)
《【译】React运用机能优化》
统统的节点都被衬着了。

每一个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(),并非异常有效。

当你有许多大列表的时刻,这个要领是很有效的。能够完整跳过列表的从新衬着时一个庞大的成功。然则假如你的运用中只要一个大列表,那末如许做实在没有任何结果,由于你的任何操纵都是基于这个列表的,意味着列表中的数据肯定会有所转变,那末你完整能够跳过对更新前提的搜检。

    原文作者:痕迹绝陌路
    原文地址: https://segmentfault.com/a/1190000008376635
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞