react性能优化

React性能优化

hepeguo 2016年08月12日发布

当大家考虑在项目中使用 React 的时候,第一个问题往往是他们的应用的速度和响应是否能和非 React 版一样,每当状态改变的时候就重新渲染组件的整个子树,让大家怀疑这会不会对性能造成负面影响。React 用了一些黑科技来减少 UI 更新需要的花费较大的 DOM 操作。

使用 production 版本

如果你在你的 React app 中进行性能测试或在寻找性能问题,一定要确定你在使用 minified production build。开发者版本包括额外的警告信息,这对你在开发你的 app 的时候很有用,但是因为要进行额外的处理,所以它也会比较慢。

避免更新 DOM

React 使用虚拟 DOM,它是在浏览器中的 DOM 子树的渲染描述,这个平行的描述让 React 避免创建和操作 DOM 节点,这些远比操作一个 JavaScript 对象慢。当一个组件的 props 或 state 改变,React 会构造一个新的虚拟 DOM 和旧的进行对比来决定真实 DOM 更新的必要性,只有在它们不相等的时候,React 才会使用尽量少的改动更新 DOM。

在此之上,React 提供了生命周期函数 shouldComponentUpdate,在重新渲染机制回路(虚拟 DOM 对比和 DOM 更新)之前会被触发,赋予开发者跳过这个过程的能力。这个函数默认返回 true,让 React 执行更新。

shouldComponentUpdate: function(nextProps, nextState) {
  return true;
}

一定要记住,React 会非常频繁的调用这个函数,所以要确保它的执行速度够快。

假如你有个带有多个对话的消息应用,如果只有一个对话发生改变,如果我们在 ChatThread 组件执行 shouldComponentUpdate,React 可以跳过其他对话的重新渲染步骤。

shouldComponentUpdate: function(nextProps, nextState) {
  // TODO: return whether or not current chat thread is
  // different to former one.
}

因此,总的说,React 通过让用户使用 shouldComponentUpdate 减短重新渲染回路,避免进行昂贵的更新 DOM 子树的操作,而且这些必要的更新,需要对比虚拟 DOM。

shouldComponentUpdate 实战

这里有个组件的子树,每一个都指明了 shouldComponentUpdate 返回值和虚拟 DOM 是否相等,最后,圆圈的颜色表示组件是否需要更新。

478926453-57adb1fb25515_articlex.png

在上面的示例中,因为 C2 的 shouldComponentUpdate 返回 false,React 就不需要生成新的虚拟 DOM,也就不需要更新 DOM,注意 React 甚至不需要调用 C4 和 C5 的 shouldComponentUpdate

C1 和 C3 的 shouldComponentUpdate 返回 true,所以 React 需要向下到叶子节点检查它们,C6 返回 true,因为虚拟 DOM 不相等,需要更新 DOM。最后感兴趣的是 C8,对于这个节点,React 需要计算虚拟 DOM,但是因为它和旧的相等,所以不需要更新 DOM。

注意 React 只需要对 C6 进行 DOM 转换,这是必须的。对于 C8,通过虚拟 DOM 的对比确定它是不需要的,C2 的子树和 C7,它们甚至不需要计算虚拟 DOM,因为 shouldComponentUpdate

那么,我们怎么实现 shouldComponentUpdate 呢?比如说你有一个组件仅仅渲染一个字符串:

React.createClass({
  propTypes: {
    value: React.PropTypes.string.isRequired
  },

  render: function() {
    return <div>{this.props.value}</div>;
  }
});

我们可以简单的实现 shouldComponentUpdate 如下:

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

非常好!处理这样简单结构的 props/state 很简单,我门甚至可以归纳出一个基于浅对比的实现,然后把它 Mixin 到组件中。实际上 React 已经提供了这样的实现: PureRenderMixin

但是如果你的组件的 props 或者 state 是可变的数据结构呢?比如说,组件接收的 prop 不是一个像 'bar' 这样的字符串,而是一个包涵字符串的 JavaScript 对象,比如 { foo: 'bar' }:

React.createClass({
  propTypes: {
    value: React.PropTypes.object.isRequired
  },

  render: function() {
    return <div>{this.props.value.foo}</div>;
  }
});

前面的 shouldComponentUpdate 实现就不会一直和我们期望的一样工作:

// assume this.props.value is { foo: 'bar' }
// assume nextProps.value is { foo: 'bar' },
// but this reference is different to this.props.value
this.props.value !== nextProps.value; // true

这个问题是当 prop 没有改变的时候 shouldComponentUpdate 也会返回 true。为了解决这个问题,我们有了这个替代实现:

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

基本上,我们结束了使用深度对比来确保改变的正确跟踪,这个方法在性能上的花费是很大的,因为我们需要为每个 model 写不同的深度对比代码。就算这样,如果我们没有处理好对象引用,它甚至不能工作,比如说这个父组件:

React.createClass({
  getInitialState: function() {
    return { value: { foo: 'bar' } };
  },

  onClick: function() {
    var value = this.state.value;
    value.foo += 'bar'; // ANTI-PATTERN!
    this.setState({ value: value });
  },

  render: function() {
    return (
      <div>
        <InnerComponent value={this.state.value} />
        <a onClick={this.onClick}>Click me</a>
      </div>
    );
  }
});

内部组件第一次渲染的时候,它会获取 { foo: 'bar' } 作为 value 的值。如果用户点击了 a 标签,父组件的 state 会更新成 { value: { foo: 'barbar' } },触发内部组件的重新渲染过程,内部组件会收到 { foo: 'barbar' } 作为 value 的新的值。

这里的问题是因为父组件和内部组件共享同一个对象的引用,当对象在 onClick 函数的第二行发生改变的时候,内部组件的属性也发生了改变,所以当重新渲染过程开始,shouldComponentUpdate 被调用的时候,this.props.value.foonextProps.value.foo 是相等的,因为实际上 this.props.valuenextProps.value 是同一个对象的引用。

因此,我们会丢失 prop 的改变,缩短重新渲染过程,UI 也不会从 'bar' 更新到 'barbar'

Immutable-js 来救赎

Immutable-js 是 Lee Byron 写的 JavaScript 集合类型的库,最近被 Facebook 开源,它通过结构共享提供不可变持久化集合类型。一起看下这些特性的含义:

  • Immutable: 一旦创建,集合就不能再改变。

  • Persistent: 新的集合类型可以通过之前的集合创建,比如 set 产生改变的集合。创建新的集合之后源集合仍然有效。

  • Structural Sharing: 新的集合会使用尽量多的源集合的结构,减少复制来节省空间和性能友好。如果新的集合和源集合相等,一般会返回源结构。

不可变让跟踪改变非常简单;每次改变都是产生新的对象,所以我们仅需要对象的引用是否改变,比如这段简单的 JavaScript 代码:

var x = { foo: "bar" };
var y = x;
y.foo = "baz";
x === y; // true

尽管 y 被改变,因为它和 x 引用的是同一个对象,这个对比返回 true。然而,这个代码可以使用 immutable-js 改写如下:

var SomeRecord = Immutable.Record({ foo: null });
var x = new SomeRecord({ foo: 'bar'  });
var y = x.set('foo', 'baz');
x === y; // false

这个例子中,因为改变 x 的时候返回了新的引用,我们就可以安全的认为 x 已经改变。

脏检测可以作为另外的可行的方式追踪改变,给 setters 一个标示。这个方法的问题是,它强制你使用 setters,而且要写很多额外的代码,影响你的类。或者你可以在改变之前深拷贝对象,然后进行深对比来确定是不是发生了改变。这个方法的问题是,深拷贝和深对比都是很花性能的操作。

因此,不可变数据结构给你提供了一个高效、简洁的方式来跟踪对象的改变,而跟踪改变是实现 shouldComponentUpdate 的关键。所以,如果我们使用 immutable-js 提供的抽象创建 props 和 state 模型,我们就可以使用 PureRenderMixin,而且能够获得很好的性能增强。

Immutable-js 和 Flux

如果你在使用 Flux,你应该开始使用 immutable-js 写你的 stores,看一下 full API

让我们看一个可行的方式,使用不可变数据结构来给消息示例创建数据结构。首先我们要给每个要建模的实体定义一个 Record。Records 仅仅是一个不可变容器,里面保存一系列具体数据:

var User = Immutable.Record({
  id: undefined,
  name: undefined,
  email: undefined
});

var Message = Immutable.Record({
  timestamp: new Date(),
  sender: undefined,
  text: ''
});

Record 方法接收一个对象,来定义字段和对应的默认数据。

消息的 store 可以使用两个 list 来跟踪 users 和 messages:

this.users = Immutable.List();
this.messages = Immutable.List();

实现函数处理每个 payload 类型应该是比较简单的,比如,当 store 看到一个代表新消息的 payload 时,我们就创建一个新的 record,并放入消息列表:

this.messages = this.messages.push(new Message({
  timestamp: payload.timestamp,
  sender: payload.sender,
  text: payload.text
});

注意:因为数据结构不可变,我们需要把 push 方法的结果赋给 this.messages

在 React 里,如果我们也使用 immutable-js 数据结构来保存组件的 state,我门可以把 PureRenderMixin 混入到我门所有的组件来缩短重新渲染回路。

这篇文章是翻译React官方文档

https://segmentfault.com/a/1190000006254212

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容