前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >react组件性能优化探索实践

react组件性能优化探索实践

作者头像
IMWeb前端团队
发布2017-12-29 16:29:08
1.2K0
发布2017-12-29 16:29:08
举报
文章被收录于专栏:IMWeb前端团队

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

react组件渲染

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

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

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

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

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

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

shouldComponentUpdate

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

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

根据渲染流程,首先会判断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版本中加入了无状态组件,如下:

代码语言:javascript
复制
// es5
function HelloMessage(props) {
  return <div>Hello {props.name}</div>;
}
代码语言:javascript
复制
// es6
const HelloMessage = (props) => <div>Hello {props.name}</div>;

具体可参考官网:Reusable Components

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

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

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

代码语言:javascript
复制
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是否改变来确定是否需要更新:

代码语言:javascript
复制
shouldComponentUpdate: function(nextProps, nextState) {
  return nextProps.id !== this.props.id;
}

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

代码语言:javascript
复制
// 原始类型
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官方有了插件帮我们搞定这事:

代码语言:javascript
复制
var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div className={this.props.className}>foo</div>;
  }
});
代码语言:javascript
复制
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,那就得想办法处理,能不能把前后的数据变成不一样的引用呢,那样不就不相等了吗?于是就有了我们的不可变数据。

代码语言:javascript
复制
var update = require('react-addons-update');

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

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

其API如下:

  • 直接改变引用
代码语言:javascript
复制
const newValue = {
    ...oldValue
    // 在这里做你想要的修改
};

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

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

newValue2 === oldValue; // false

然后在shouldComponentUpdate中进行比对

代码语言:javascript
复制
shouldComponentUpdate(nextProps) {
    return isObjectEqual(this.props, nextProps);
}
代码语言:javascript
复制
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等,而不是判断整个对象:

代码语言:javascript
复制
shouldComponentUpdate: function(nextProps, nextState){
    return nextProps.title !== this.props.title;
}

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

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

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

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

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

列表类组件优化

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

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

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

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

代码语言:javascript
复制
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值之后

代码语言:javascript
复制
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
代码语言:javascript
复制
import Perf from 'react-addons-perf';
  • 下面你可以通过console面板或者下载chrome 插件React Perf来调试,这里以console面板为例:

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

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

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

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

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

图一,没有优化前

图二,优化后

其他api可到官网查阅。

参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • react组件渲染
  • shouldComponentUpdate
  • 牛刀小试,直接把一些不需要更新的组件返回false
  • 登堂入室,对数据进行对比确定是否需要更新
    • 原始类型数据
      • 引用类型数据
      • 列表类组件优化
      • 如何使用perf分析组件性能
      • 参考资料
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档