资讯专栏INFORMATION COLUMN

【译】React应用性能优化

txgcwm / 318人阅读

摘要:应用主要的的性能瓶颈来自于一些冗余的程序处理以及组件中的的过程。为了避免这种情况,在你的应用中尽可能多的让返回。使用工具将帮助你找到应用程序中特定的性能问题。这个工具跟用起来很像,但是它是专门用来检测应用性能的。

这段时间对自己写的React应用的性能做了一些分析以及优化,发现项目中产生性能问题的原因主要来自两个方面:

大量的数据渲染使组件进行不必要的diff过程,导致应用卡顿;

部分交互操作频繁的组件中使用了一些不必要的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

并且你还要加快这个过程:

shouldComponentUpdate中的条件判断应该尽可能的快

shouldComponentUpdate中的条件判断要尽可能的简单

以下是正文

是什么造成了React应用的性能瓶颈?

不需要更新DOM的冗余处理

对大量不需要更新的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

并且你还要加快这个过程:

shouldComponentUpdate中的条件判断应该尽可能的快

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() {
        
{this.props.items.map(item => { const isSelected = this.props.interaction.selectedId === item.id; return (); })}
} }
问题1:庞大的shouldComponentUpdate函数

从上面的例子就可以看出来,即便是那么一小段很简单的数据结构,shouldConponentUpdate函数依然有如此繁杂的处理。这是因为这个函数需要了解数据结构,以及每个数据之间又怎样的关联。所以说,shouldComponentUpdate函数的大小和复杂度,是由数据结构决定的。

这很容易引起两个错误:

不该返回false时,返回了false(状态在程序中没有被正确处理,导致视图不更新)

不该返回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() {
            
{this.props.items.map(item => { return ( ); })}
} }

如果你想要更新其中的一个数据,比如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 
    }
}

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;

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

这是在创建函数中经常使用的。

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

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

        
{items.map(item => { // this function will have a new reference every time const callback = () => { dispatch(myActionCreator(item)); } return })}
} }
解决这个问题的策略

避免在组件内部创建动态props(改善你的数据结构,使props可以被直接用来传递)

将动态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;

    
{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 })}
}
特例:函数

尽量不要传递函数。在子组件需要时才去触发相应的actions。这样做还有一个好处是将业务逻辑与组件分离开来。

忽略shouldComponentUpdate函数中对functions的检查,因为我们无法知晓函数的值是否发生改变。

创建一个不可变数据与函数的映射。你可以在执行componentWillReveiveProps函数时,把这个映射放到state中。这样的话每次render时将不会得到一个新的引用,便于执行在shouldComponentUpdate时的引用检查。这个方法比较麻烦,因为需要维护和更新函数列表。

创建一个有正确this绑定的中间组件。这样也并不理想,因为在组件的层次结构中引入了冗余层。(实际上作者的意思是将函数的定义从render函数中移出,这样每次的render就不会创建新的引用了)

避免每次执行render函数时,都创建一个新的函数。

关于第四点的例子:

// introduce another layer "ListItem"

     // you can create the correct this bindings in here
        
    


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 
    }
}
工具

上面列出的所有规则和技术都是通过使用性能测量工具发现的。 使用工具将帮助你找到应用程序中特定的性能问题。

console.time

这个工具相当简单。

开始计时

程序运行

结束计时

一个很棒的方式是用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应用性能的。

Perf.start

程序运行

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://www.ucloud.cn/yun/81538.html

相关文章

  • [] 唯快不破:Web 应用的 13 个优化步骤

    摘要:译文地址译唯快不破应用的个优化步骤前端的逆袭知乎专栏原文地址时过境迁,应用比以往任何时候都更具交互性。使用负载均衡方案我们在之前讨论缓存的时候简要提到了内容分发网络。换句话说,元素的串形访问会削弱负载均衡器以最佳形式 欢迎关注知乎专栏 —— 前端的逆袭欢迎关注我的博客,知乎,GitHub。 译文地址:【译】唯快不破:Web 应用的 13 个优化步骤 - 前端的逆袭 - 知乎专栏原文地...

    haobowd 评论0 收藏0
  • 前端优化 - 收藏集 - 掘金

    摘要:虽然有着各种各样的不同,但是相同的是,他们前端优化不完全指南前端掘金篇幅可能有点长,我想先聊一聊阅读的方式,我希望你阅读的时候,能够把我当作你的竞争对手,你的梦想是超越我。 如何提升页面渲染效率 - 前端 - 掘金Web页面的性能 我们每天都会浏览很多的Web页面,使用很多基于Web的应用。这些站点看起来既不一样,用途也都各有不同,有在线视频,Social Media,新闻,邮件客户端...

    VincentFF 评论0 收藏0
  • 】展示型组件和容器型组件(作者:Dan Abramov,Redux的开发者)

    摘要:我的教程可能也会帮你一把其他的二分法展示型组件和容器型组件这种分类并非十分严格,这是按照它们的目的进行分类。在我看来,展示型组件往往是无状态的纯函数组件,容器型组件往往是有状态的纯类组件。不要把展示型组件和容器型组件的划分当成教条。 本文译自Presentational and Container Components,文章的作者是Dan Abramov,他同时也是Redux和Crea...

    lily_wang 评论0 收藏0
  • 前端优化感想以及[]redux 教程 第一部分(共四部分

    摘要:自己英语一般,水平有限,献上原文地址,还有我翻译的中文地址,欢迎大家勘误下面是自己的一点感想先说一下,我们知道,前端优化有这么几步,第一步首先呢我们知道,一个应用要依赖好多条文件,而浏览器加载完一条,要执行完这条才加载下一条,所以呢,就很慢 自己英语一般,水平有限,献上原文地址,还有我翻译的中文地址,欢迎大家勘误 下面是自己的一点感想 先说一下webpack,我们知道,前端优化有这么几...

    snowell 评论0 收藏0
  • 】JavaScript 框架的探索与变迁(下)

    摘要:对此没有任何限制,它不关心这个。一种控制变化的办法是不可改变的,持久化的数据结构。总结检测变化时开发中的核心问题,而框架们以各种方式解决这个问题。因为组件内的变化是不被允许的。 AngularJS:脏检查 我不知道什么更新了,所以当更新的时候,我只能检查所有的东西。 AngularJS 类似于 Ember,当状态改变的时候,必须人工去处理。但不同的是,AngularJS 从不同的角度来...

    CollinPeng 评论0 收藏0

发表评论

0条评论

txgcwm

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<