React性能优化总结

MadAvent 3年前
   <p>初学者对React可能满怀期待,觉得React可能完爆其它一切框架,甚至不切实际地认为React可能连原生的渲染都能完爆——对框架的狂热确实会出现这样的不切实际的期待。让我们来看看React的官方是怎么说的。React官方文档在Advanced Performanec这一节,这样写道:</p>    <p>One of the first questions people ask when considering React for a project is whether their application will be as fast and responsive as an equivalent non-React version</p>    <p>显然React自己也其实只是想尽量达到跟非React版本相若的性能,</p>    <h2>你所不知道的render</h2>    <p>react的组件渲染分为初始化渲染和更新渲染。</p>    <p>在初始化渲染的时候会调用根组件下的所有组件的render方法进行渲染,如下图(绿色表示已渲染,这一层是没有问题的):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a555ae15aef9ea16236028dd4b6426e6.jpg"></p>    <p>但是当我们要更新某个子组件的时候,如下图的绿色组件(从根组件传递下来应用在绿色组件上的数据发生改变):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ae2b743df461aee36f826a954cfa6dc7.jpg"></p>    <p>我们的理想状态是只调用关键路径上组件的render,如下图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/57da54a590c4d964a5499d25322dbcac.jpg"></p>    <p>但是react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比,如不变则不进行更新。这样的render和虚拟DOM的 对比 明显是在浪费,如下图(黄色表示浪费的render和虚拟DOM对比)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/265da68e74c2408439a71771cfcb548b.jpg"></p>    <p>Tips:</p>    <ul>     <li> <p>拆分组件是有利于复用和组件优化的。</p> </li>     <li> <p>生成虚拟DOM并进行比对发生在render()后,而不是render()前。</p> </li>    </ul>    <h3>更新阶段的生命周期</h3>    <ul>     <li> <p>componentWillReceiveProps(object nextProps) :当挂载的组件接收到新的props时被调用。此方法应该被用于比较this.props 和 nextProps以用于使用this.setState()执行状态转换。(组件内部数据有变化,使用state,但是在更新阶段又要在props改变的时候改变state,则在这个生命周期里面)</p> </li>     <li> <p>shouldComponentUpdate(object nextProps, object nextState) : -boolean 当组件决定任何改变是否要更新到DOM时被调用。作为一个 优化 实现比较this.props 和 nextProps 、this.state 和 nextState ,如果React应该跳过更新,返回false。</p> </li>     <li> <p>componentWillUpdate(object nextProps, object nextState) :在更新发生前被立即调用。你不能在此调用 this.setState() 。</p> </li>     <li> <p>componentDidUpdate(object prevProps, object prevState) : 在更新发生后被立即调用。(可以在DOM更新完之后,做一些收尾的工作)</p> </li>    </ul>    <p>Tips:</p>    <ul>     <li> <p>React的优化是基于 shouldComponentUpdate 的,该生命周期默认返回true,所以一旦prop或state有任何变化,都会引起重新render。</p> </li>    </ul>    <h3>shouldComponentUpdate</h3>    <p>react在每个组件生命周期更新的时候都会调用一个shouldComponentUpdate(nextProps, nextState)函数。它的职责就是返回true或false,true表示需要更新,false表示不需要,默认返回为true,即便你没有显示地定义 shouldComponentUpdate 函数。这就不难解释上面发生的资源浪费了。</p>    <p>为了进一步说明问题,我们再引用一张官网的图来解释,如下图( SCU表示shouldComponentUpdate,绿色表示返回true(需要更新),红色表示返回false(不需要更新);vDOMEq表示虚拟DOM比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7123035556b39201efef905acfaa148e.jpg"></p>    <p>根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。</p>    <ul>     <li> <p>C1根节点,绿色SCU (true),表示需要更新,然后vDOMEq红色,表示虚拟DOM不一致,需要更新。</p> </li>     <li> <p>C2节点,红色SCU (false),表示不需要更新,所以C4,C5均不再进行检查</p> </li>     <li> <p>C3节点同C1,需要更新</p> </li>     <li> <p>C6节点,绿色SCU (true),表示需要更新,然后vDOMEq红色,表示虚拟DOM不一致,更新DOM。</p> </li>     <li> <p>C7节点同C2</p> </li>     <li> <p>C8节点,绿色SCU (true),表示需要更新,然后vDOMEq绿色,表示虚拟DOM一致,不更新DOM。</p> </li>    </ul>    <h2>带坑的写法:</h2>    <ul>     <li> <p>{...this.props} (不要滥用,请只传递component需要的props,传得太多,或者层次传得太深,都会加重shouldComponentUpdate里面的数据比较负担,因此,也请慎用spread attributes(<Component {...props} />))。</p> </li>     <li> <p>::this.handleChange()。(请将方法的bind一律置于constructor)</p> </li>     <li> <p>this.handleChange.bind(this,id)</p> </li>     <li> <p>复杂的页面不要在一个组件里面写完。</p> </li>     <li> <p>请尽量使用const element。</p> </li>     <li> <p>map里面添加key,并且key不要使用index(可变的)。</p> </li>     <li> <p>尽量少用setTimeOut或不可控的refs、DOM操作。</p> </li>     <li> <p>数据尽可能简单明了,扁平化。</p> </li>    </ul>    <h2>性能检测工具</h2>    <h3>React.addons.Perf</h3>    <p>react官方提供一个插件 React.addons.Perf 可以帮助我们分析组件的性能,以确定是否需要优化。</p>    <p>打开console面板,先输入 Perf.start() 执行一些组件操作,引起数据变动,组件更新,然后输入 Perf.stop() 。(建议一次只执行一个操作,好进行分析)</p>    <p>再输入 Perf.printInclusive 查看所有涉及到的组件render,如下图(官方图片):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/64c541802080572554d22590c8251dbd.png"></p>    <p>或者输入Perf.printWasted()查看下不需要的的浪费组件render,如下图(官方图片):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a2804863c6f6a17f5b2511a643accb13.png"></p>    <p>优化前:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fd96eebf3f461f4cd396b4dc64b9ac5b.png"></p>    <p>优化后:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/15057ed52bfe2d4c9b27ec357297d5df.png"></p>    <h3>其他的检测工具</h3>    <p><a href="/misc/goto?guid=4959729740407680158" rel="nofollow,noindex">react-perf-tool</a> 为React应用提供了一种可视化的性能检测方案,该工程同样是基于React.addons,但是使用图表来显示结果,更加方便。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fe103ef611b7a35ece4a71047c8d25e4.jpg"></p>    <h2>React官方的解决方案</h2>    <h3>PureRenderMixin(es5)</h3>    <pre>  <code class="language-javascript">var PureRenderMixin = require('react-addons-pure-render-mixin');  React.createClass({    mixins: [PureRenderMixin],      render: function() {      return <div className={this.props.className}>foo</div>;    }  });</code></pre>    <h3>Shallow Compare (es6)</h3>    <pre>  <code class="language-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>;    }  }</code></pre>    <p>es7装饰器的写法:</p>    <pre>  <code class="language-javascript">import pureRender from "pure-render-decorator"  ...    @pureRender  class Person  extends Component {    render() {      console.log("我re-render了");      const {name,age} = this.props;          return (          <div>            <span>姓名:</span>            <span>{name}</span>            <span> age:</span>            <span>{age}</span>          </div>        )    }  }</code></pre>    <p>pureRender很简单,就是把传进来的component的shouldComponentUpdate给重写掉了,原来的shouldComponentUpdate,无论怎样都是return ture,现在不了,我要用shallowCompare比一比,shallowCompare代码及其简单,如下:</p>    <pre>  <code class="language-javascript">function shallowCompare(instance, nextProps, nextState) {    return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);  }</code></pre>    <h3>缺点</h3>    <p>shallowEqual其实只比较props的第一层子属性是不是相同,就像上述代码,props 是如下</p>    <pre>  <code class="language-javascript">{    detail: {      name: "123",      age: "123"    }  }</code></pre>    <p>他只会比较props.detail ===nextProps.detail,导致在传入复杂的数据的情况下,优化失效。</p>    <h2>immutable.js</h2>    <p>我们也可以在 shouldComponentUpdate() 中使用使用 deepCopy 和 deepCompare 来避免无必要的 render(),但 deepCopy 和 deepCompare 一般都是非常耗性能的。</p>    <p>Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。</p>    <p>Immutable 实现的原理是 Persistent Data Structure (持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing (结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/38ffaa6822fa66c12ac56c25ccc7b1de.gif"></p>    <p>Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 === 和 is 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的:</p>    <pre>  <code class="language-javascript">import { is } from 'immutable';    shouldComponentUpdate: (nextProps = {}, nextState = {}) => {    const thisProps = this.props || {}, thisState = this.state || {};      if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||        Object.keys(thisState).length !== Object.keys(nextState).length) {      return true;    }      for (const key in nextProps) {      if (!is(thisProps[key], nextProps[key])) {        return true;      }    }      for (const key in nextState) {      if (thisState[key] !== nextState[key] || !is(thisState[key], nextState[key])) {        return true;      }    }    return false;  }</code></pre>    <h3>react-immutable-render-mixin</h3>    <p>这是一个非死book/immutable-js的react pure render mixin 的库,可以简化很多写法。</p>    <p>使用 react-immutable-render-mixin 可以实现装饰器的写法。</p>    <pre>  <code class="language-javascript">import React from 'react';  import { immutableRenderDecorator } from 'react-immutable-render-mixin';    @immutableRenderDecorator  class Test extends React.Component {    render() {      return <div></div>;    }  }</code></pre>    <h2>无状态组件</h2>    <p>为了避免一定程度的浪费,react官方还在0.14版本中加入了 无状态组件 ,如下:</p>    <pre>  <code class="language-javascript">// es6  const HelloMessage = (props) => <div>Hello {props.name}</div>;</code></pre>    <h2>高阶组件</h2>    <p>大部分使用mixin和class extends的地方,高阶组件都是更好的方案——毕竟 组合优于继承 。</p>    <h2>React同构直出</h2>    <p>同构基于服务端渲染,却不止是服务端渲染。</p>    <p>React在减少重复渲染方面确实是有一套独特的处理办法,那就是virtual DOM,但显示在首次渲染的时候React绝无可能超越原生的速度,或者一定能将其它的框架比下去。因此,我们在做优化的时候,可的期待的东西有:</p>    <ul>     <li> <p>首屏时间可能会比较原生的慢一些,但可以尝试用React Server Render (又称Isomorphic)去提高效率</p> </li>     <li> <p>用户进行交互的时候,有可能会比原生的响应快一些,前提是你做了一些优化避免了浪费性能的重复渲染。</p> </li>    </ul>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007811296</p>    <p> </p>