React纯组件渲染性能反模式

React纯组件渲染性能反模式

React纯组件的渲染可以非常高效,但是需要用户将其数据作为不可变的对象,才能正常工作。但是由于JavaScript的原因,有时做到这点可能非常具有挑战性。

反模式是在Render函数或者Redux的connect(mapState)中创建新的数组、对象、函数或者其他新的对象

纯渲染?

说起React的纯渲染,我指的是组件应该通过浅比较来实现shouldComponentUpdate方法。例如PureRenderMixinrecompose/pure ,等等

为什么?

这可能是你能在React中做的最显著的一个性能优化。这也是ClojureScript默认对React所做的封装,并且声称其比普通的React速度快的原因。因为ClojureScript必须使用不变的数据结构来存储state,所以在判断是否重新渲染组件时花费很小。然而使用可变的数据来深比较数据是否相等将非常耗费性能。在ClojureScript中,这很简单,因为所有的对象总是不可变的,但是在Javascript中不是如此。

公平的说,即使没有使用纯渲染优化,React也会很快,但是当使用基于JavaScript的动画(例如react-motion),在1s内组件会被成千上万次渲染,或者使用大的组件,例如有上千个单元格的可编辑的表格,在这些情况下,优化将变得至关重要。同样在低配置的移动设备中,你会从中得到巨大的性能提升。

反模式

几个月之前,我写了一个可编辑的表格,用来从电子表格中导入用户数据。一张表格很容易就有超过500个用户。在最上层的组件中,我写的代码如下:

class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || []} />
         )}
       </div>
     );
  }
}

实际上,代码比这更加复杂。Cell组件非常复杂,对于每个用户渲染了好多的单元格。所以在应用中有上千个Cell元素。

在应用中我载入了500个用户,并且尝试修改一个单元格,修改的动作在我高性能的电脑上竟然花费了几秒时间才完成!后来使用了console.log()来调试代码后,我发现当一个很小的单元格改变后,几乎整个应用都会被重新渲染。这怎么可能?我使用的是Redux,冻结了应用的状态,并且使用了不可变的数据。

经过几个小时抓破头皮的思考,我意识到,这其中的一个改变时我使用的数组的默认值:

    this.props.options || []

可以看到options数组传递给Cell元素。通常来说,这没有任何问题。其他的Cell元素也不会被渲染,因为他们可以做浅比较来检查属性是否一致,并且在一致时跳过渲染,但是万一props是null,就会使用默认的数组。正如你所知道的那样,数组字面量和new Array()都会创建一个新的数组实例。这会彻底的破坏掉Cell元素内纯组件渲染优化。Javascript的不同实例是不相等的,浅比较是否相等总是会返回false,并且告诉React来重新渲染组件。修改的方法非常简单:

const default = [];
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || default} />
         )}
       </div>
     );
  }
}

现在修改操作只需要几十毫秒!并且defaultProps的作用和以前一样。

函数也会创建新对象

在render中创建函数也会有同样的问题,好多代码是如下这样写的:

class App extends PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)} />;
  }
}

或者

class App extends PureComponent {
  update(e) {
    this.props.update(e.target.value);
  }
  render() {
    return <MyInput onChange={this.update.bind(this)} />;
 }
}

和上面的数组字面量类似,在这两种情况下,都会创建一个新的函数对象。你应该尽早的执行绑定this

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.update = this.update.bind(this);
  }
  update(e) {
    this.props.update(e.target.value);
  }
  render() {
    return <MyInput onChange={this.update} />;
  }
}

还需要重复一点。也有其他的方法来解决这个问题,使用React.createClass()来自动绑定所有的方法或者使用Babel来箭头函数,还有使用自动绑定装饰器。

ESLint rule react/jsx-no-bind 是一个用来捕获该问题的工具。

在Reducconnect(mapState)中使用Reselect

起初,我并不认为Reselect(一个在Redux官方文档中提到的类库)会如此重要,因为我很少在Reduxconnect()方法中写性能低下的map state函数。我错的是如此离谱。这和函数的性能没有关系,关键是新对象(吃惊吧!),考虑如下的map state函数:

let App = ({otherData, resolution}) => (
  <div>
    <DataContainer data={otherData} />
    <ResolutionContainer resolution={resolution} />
  </div>
);
const doubleRes = (size) => ({
  width: size.width*2,
  height: size.height*2
});
App = connect(state => {
  return {
    otherData: state.otherData,
    resolution: doubleRes(state.resolution)
  }
})(App);

在这个例子中,state中otherData每次发生变化,DataContainerResolutionContainer都会重新渲染,即使state中的resolution没有发生变化。这是因为函数doubleRes总是会返回一个新的resolution对象。如果使用Reselect重写doubleRes,问题就会变为如下的情况:

import {createSelector} from “reselect”;
const doubleRes = createSelector(
  r => r.width,
  r => r.height,
  (width, height) => ({
    width: width*2,
    height: heiht*2
  })
);

Reselect会记住上一次函数的结果,在传入参数没有改变的情况下,将其返回。

结论

当你注意到的时候,反模式很明显,但是仍然很容易陷进去。比较好的方面是,如果你搞砸了,就像我之前那样,这不会破坏你的应用,只是会运行的比较慢一点,大多数情况下,并不重要。但是我希望在这篇文章中给你指向了应该去深入研究的某些内容。

翻译自React.js pure render performance anti-pattern

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

推荐阅读更多精彩内容