结一

2016-06-30 20:39

react组件性能优化探索实践

本文作者:IMWeb 结一 原文出处:IMWeb社区 未经同意,禁止转载

React本身就非常关注性能,其提供的虚拟DOM搭配上Diff算法,实现对DOM操作最小粒度的改变也是非常的高效。然而其组件渲染机制,也决定了在对组件进行更新时还可以进行更细致的优化。

react组件渲染

react的组件渲染分为初始化渲染和更新渲染。

在初始化渲染的时候会调用根组件下的所有组件的render方法进行渲染,如下图(绿色表示已渲染,这一层是没有问题的):

react 组件渲染 初始化渲染

但是当我们要更新某个子组件的时候,如下图的绿色组件(从根组件传递下来应用在绿色组件上的数据发生改变):

react 组件渲染 更新子组件

我们的理想状态是只调用关键路径上组件的render,如下图:

react 组件渲染 理想渲染

但是react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比,如不变则不进行更新。这样的render和虚拟DOM的对比明显是在浪费,如下图(黄色表示浪费的render和虚拟DOM对比)

react 组件渲染 实际渲染

那么如何避免发生这个浪费问题呢,这就要牵出我们的shouldComponentUpdate

shouldComponentUpdate

react在每个组件生命周期更新的时候都会调用一个shouldComponentUpdate(nextProps, nextState)函数。它的职责就是返回true或false,true表示需要更新,false表示不需要,默认返回为true,即便你没有显示地定义 shouldComponentUpdate 函数。这就不难解释上面发生的资源浪费了。

为了进一步说明问题,我们再引用一张官网的图来解释,如下图( SCU表示shouldComponentUpdate,绿色表示返回true(需要更新),红色表示返回false(不需要更新);vDOMEq表示虚拟DOM比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):

shouldComponentUpdate

根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。

  • C1根节点,绿色SCU (true),表示需要更新,然后vDOMEq红色,表示虚拟DOM不一致,需要更新。
  • C2节点,红色SCU (false),表示不需要更新,所以C4,C5均不再进行检查
  • C3节点同C1,需要更新
  • C6节点,绿色SCU (true),表示需要更新,然后vDOMEq红色,表示虚拟DOM不一致,更新DOM。
  • C7节点同C2
  • C8节点,绿色SCU (true),表示需要更新,然后vDOMEq绿色,表示虚拟DOM一致,不更新DOM。

为了避免一定程度的浪费,react官方还在0.14版本中加入了无状态组件,如下:

// es5
function HelloMessage(props) {
  return <div>Hello {props.name}</div>;
}
// es6
const HelloMessage = (props) => <div>Hello {props.name}</div>;

具体可参考官网:Reusable Components

既然明白了这关键所在,现在是时候向我们的大大小小一箩筐组件开刀了。

牛刀小试,直接把一些不需要更新的组件返回false

下面我们以音量图标为例,这是一个svg图标,不需要更新,所以直接return false

import React, {Component} from 'react';

class Mic extends Component {
    constructor(props) {
      super(props);
    }
    shouldComponentUpdate() {
        return false;
    }
    render() {
        return (
            <svg className="icon-svg icon-mic" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
                <title>mic</title>
                <path className="path1" d="M15 22c2.761 0 5-2.239 5-5v-12c0-2.761-2.239-5-5-5s-5 2.239-5 5v12c0 2.761 2.239 5 5 5zM22 14v3c0 3.866-3.134 7-7 7s-7-3.134-7-7v-3h-2v3c0 4.632 3.5 8.447 8 8.944v4.056h-4v2h10v-2h-4v-4.056c4.5-0.497 8-4.312 8-8.944v-3h-2z"></path>
            </svg>
        )
    }
}

export default Mic;

登堂入室,对数据进行对比确定是否需要更新

先来个官网的例子,通过判断id是否改变来确定是否需要更新:

shouldComponentUpdate: function(nextProps, nextState) {
  return nextProps.id !== this.props.id;
}

看起来也没那么玄乎,直接一个!==对比下就ok了,那是不是所有的都可以这样直接对比就可以呢? 我们先来看下js的两个数据类型(原始类型与引用类型)的各自比较

// 原始类型
var a = 'hello the';
var b = a;
b = b + 'world';
console.log(a === b);  // false

// 引用类型
var c = ['hello', 'the'];
var d = c;
d.push('world');
console.log(c === d); // true

我们可以看到a和b不等,但是c和d是一样一样的,这修改了d,也直接修改了c,那还怎么对比(关于原始类型与引用类型的区别这里就不说明了)。

现在看来我们得分情况处理了,原始类型数据和引用类型数据得采用不同的办法处理。

原始类型数据

这没什么好说的,直接比对就是了。但是每个人都是想偷懒的,这要是每个组件都要这样去写下也挺麻烦的,于是react官方有了插件帮我们搞定这事:

var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div className={this.props.className}>foo</div>;
  }
});
var shallowCompare = require('react-addons-shallow-compare');
export class SampleComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }

  render() {
    return <div className={this.props.className}>foo</div>;
  }
}

引用类型数据

既然引用类型数据一直返回true,那就得想办法处理,能不能把前后的数据变成不一样的引用呢,那样不就不相等了吗?于是就有了我们的不可变数据。

var update = require('react-addons-update');

var newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});

这样newData与myData就可以对比了。

其API如下:

react addons update api

  • 直接改变引用
const newValue = {
    ...oldValue
    // 在这里做你想要的修改
};

// 快速检查 —— 只要检查引用
newValue === oldValue; // false

// 如果你愿意也可以用 Object.assign 语法
const newValue2 = Object.assign({}, oldValue);

newValue2 === oldValue; // false

然后在shouldComponentUpdate中进行比对

shouldComponentUpdate(nextProps) {
    return isObjectEqual(this.props, nextProps);
}
const isObjectEqual = (obj1, obj2) => {
    if(!isObject(obj1) || !isObject(obj2)) {
        return false;
    }

    // 引用是否相同
    if(obj1 === obj2) {
        return true;
    }

    // 它们包含的键名是否一致?
    const item1Keys = Object.keys(obj1).sort();
    const item2Keys = Object.keys(obj2).sort();

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

    // 属性所对应的每一个对象是否具有相同的引用?
    return item2Keys.every(key => {
        const value = obj1[key];
        const nextValue = obj2[key];

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

        // 数组例外,再检查一个层级的深度
        return Array.isArray(value) && 
            Array.isArray(nextValue) && 
            isArrayEqual(value, nextValue);
    });
};

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

    // 检查一个层级深度
    return array1.length === array2.length &&
        array1.every((item, index) => item === array2[index]);
};

我们目前采用的是在reducer里面更新数据使用Object.assign({}, state, {newkey: newValue}(数据管理采用redux),然后在组件里面根据某个具体的字段判断是否更新,如title或id等,而不是判断整个对象:

shouldComponentUpdate: function(nextProps, nextState){
    return nextProps.title !== this.props.title;
}

(表示这个js太大了,所以我也没有具体实践过。)

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:

immutable

具体如何使用可参考下面两篇文章:

至此,shouldComponentUpdate优化介绍完毕,我们接着进入另一个需要的优化点:列表类组件

列表类组件优化

列表类组件默认更新方式会比较复杂(因为可能会涉及到增删改,排序等复杂操作),所以需要加上一个key属性,提供一种除组件类之外的识别一个组件的方法。

如果某个组件key值发生变化,React会直接跳过DOM diff,重新渲染,从而节省计算提高性能。

key值除了告诉React什么时候抛弃diff直接重新渲染之外,更多的情况下可用于列表顺序发生改变的时候(如删除某项,插入某项,数据某个特定字段顺序或倒序显示),可以根据key值的位置直接调整DOM顺序。

如下例,根据时间排序图片(没有key值):

var items = sortBy(this.state.sortingTime, this.props.items);

return items.map(function(item) {
    return <img src={item.src} />;
})

如果顺序发生改变,React会对元素进行diff操作并确定出最高效的操作是改变其中几个img元素的src属性。虽然如此,但是还是有了diff的计算时间,效率其实已经非常低了。

而如果加上key值之后

return <img src={item.src} key={item.id} />;

React得出的结论就不是diff,而是直接使用insertBefore操作,而这个操作是移动DOM节点最高效的办法。

同理如果有一老师批改的作业列表,在批改完某个作业之后,该作业item应该被移除,有了key值之后,一检查key值,发现少了一个,于是直接移除该dom节点。

需要注意的是:每个key值是唯一的,在组件内部也不能通过this.props.key获取到。

现在我们知道了如何去优化react的组件,但是优化不能光靠自己的直觉,那么有没有个什么工具可以告诉我们什么时候需要优化呢?

如何使用perf分析组件性能

react官方提供一个插件React.addons.Perf可以帮助我们分析组件的性能,以确定是否需要优化。

下面简单说下如何使用:

  • 首先引入react-addons-perf
import Perf from 'react-addons-perf';
  • 下面你可以通过console面板或者下载chrome 插件React Perf来调试,这里以console面板为例:

打开console面板,先输入Perf.start() 执行一些组件操作,引起数据变动,组件更新,然后输入Perf.stop()。(建议一次只执行一个操作,好进行分析)

再输入Perf.printInclusive查看所有涉及到的组件render,如下图(官方图片):

react perf inclusive

或者输入Perf.printWasted()查看下不需要的的浪费组件render,如下图(官方图片): react perf wasted

如果printWasted有数据,则表示可以优化,优化得好,是一个空数组,没有数据。

下图是二张我截图的对比图(截图为开发环境,通过require得到react),从第一张的Perf.printWasted()可以得到有15个浪费的render,于是我进行了一次shouldComponentUpdate优化,得到第二张图,为空数据:

图一,没有优化前

react perf

图二,优化后

react perf

其他api可到官网查阅。

参考资料

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。