react组件性能优化探索实践

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版本中加入了无状态组件,如下:

// 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如下:

  • 直接改变引用
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(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:

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

至此,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,如下图(官方图片):

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

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

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

图一,没有优化前

图二,优化后

其他api可到官网查阅。

参考资料

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏移动开发之家

Flutter完整开发实战详解(一、Dart语言和Flutter基础)

 在如今的 Fultter 大潮下,本系列是让你看完会安心的文章。本系列将完整讲述:如何快速从0开发一个完整的 Flutter APP,配套高完成度 Flut...

752
来自专栏木子墨的前端日常

easyUI datagrid 清空

最近在做一个管理系统,出于一些需要,经常要将一些datagrid清空。然后easyUI本身并没有自带的方法,然后自己动手丰衣足食吧。

643
来自专栏更流畅、简洁的软件开发方式

实体类的变形【1】—— 餐盘原理

    在亚历山大同学的post里面我说可以让实体类和表不必一一对应,但是并没有详细说明如何来做,也有人想问我是怎么做的,那么我就说一下。先说一个简单一点的,那...

1787
来自专栏前端说吧

vue-细节小知识点汇总(更新中...)

874
来自专栏极客编程

Sass 快速入门学习

  众所周知css并不能算是一们真正意义上的“编程”语言,它本身无法未完成像其它编程语言一样的嵌套、继承、设置变量等工作。

511
来自专栏JackieZheng

把玩爬虫框架Gecco

如果你现在接到一个任务,获取某某行业下的分类。 作为一个非该领域专家,没有深厚的运营经验功底,要提供一套摆的上台面且让人信服的行业分类,恐怕不那么简单。 找不到...

4694
来自专栏Jacklin攻城狮

翻译_iOS视图编程指南(View Programming Guide for iOS)之视图和窗口体系

前些日子,我发布一个苹果官方文档的翻译,之后就有不少同学朋友问我:翻译苹果官方文档能做什么,开发过程用到的时候很少,浪费时间,还又没什么用。今天,刚好有时间,就...

654
来自专栏ASP.NET MVC5 后台权限管理系统

FullCalendar 日历插件中文说明文档

FullCalendar提供了丰富的属性设置和方法调用,开发者可以根据FullCalendar提供的API快速完成一个日历日程的开发,本文将FullCalend...

4088
来自专栏郭霖

Android图片滚动,加入自动播放功能,使用自定义属性实现,霸气十足!

大家好,记得上次我带着大家一起实现了一个类似与客户端中带有的图片滚动播放器的效果,但是在做完了之后,发现忘了加入图片自动播放的功能(或许是我有意忘记加的.......

2139
来自专栏章鱼的慢慢技术路

程序员算法时间空间复杂度速查表

1265

扫码关注云+社区