本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/57908...java
最近一个季度,咱们都在为手Q家校群作重构优化,将原有那套问题不断的框架换掉。通过一些斟酌,决定使用react 进行重构。选择react,其实也主要是由于它具备下面的三大特性。node
学习React的好处就是,学了一遍以后,可以写web, node直出,以及native,可以适应各类纷繁复杂的业务。须要轻量快捷的,直接能够用Reactjs;须要提高首屏时间的,能够结合React Server Render;须要更好的性能的,能够上React Native。react
可是,这其实暗示学习的曲线很是陡峭。单单是Webpack+ React + Redux就已够一个入门者够呛,更况且还要兼顾直出和手机客户端。不是通常人能hold住全部端。webpack
Virtual Dom(下称vd)算是React的一个重大的特点,由于Facebook宣称因为vd的帮助,React可以达到很好的性能。是的,Facebook说的没错,但只说了一半,它说漏的一半是:“除非你能正确的采用一系列优化手段”。git
另外一个被你们所推崇的React优点在于,它能令到你的代码组织更清晰,维护起来更容易。咱们在写的时候也有同感,但那是直到咱们踩了一些坑,而且渐渐熟悉React+ Redux所推崇的那套代码组织规范以后。es6
那么?github
上面的描述难免有些先扬后抑的感受,那是由于每每做为React的刚入门者,都会像咱们初入的时候同样,对React满怀但愿,指意它帮咱们作好一切,但随着了解的深刻,发现须要作一些额外的事情来达到咱们的期待。web
初学者对React可能满怀期待,以为React可能完爆其它一切框架,甚至不切实际地认为React可能连原生的渲染都能完爆——对框架的狂热确实会出现这样的不切实际的期待。让咱们来看看React的官方是怎么说的。React官方文档在Advanced Performanec这一节,这样写道:算法
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 versionchrome
显然React本身也其实只是想尽可能达到跟非React版本相若的性能。React在减小重复渲染方面确实是有一套独特的处理办法,那就是vd,但显示在首次渲染的时候React绝无可能超越原生的速度,或者必定能将其它的框架比下去。所以,咱们在作优化的时候,可的期待的东西有:
首屏时间可能会比较原生的慢一些,但能够尝试用React Server Render (又称Isomorphic)去提升效率
用户进行交互的时候,有可能会比原生的响应快一些,前提是你作了一些优化避免了浪费性能的重复渲染。
手Q家校群功能页主要由三个页面构成,分别是列表页、布置页和详情页。列表页已经重构完成并已发布,布置页已重构完毕准备提测,详情页正在重构。与此同时咱们已完成对列表页的同构直出优化,并已正在作React Native优化的铺垫。
这三个页面的重构其实覆盖了很多页面的案例,因此仍是蛮有表明性的,咱们会将重构之中遇到的一些经验穿插在文章里论述。
在手Q家校群重构以前,其实咱们已经作了一版PC家校群。当时将native的页面所有web化,直接就采用了React比较经常使用的全家桶套装:
构建工具 => gulp + webpack
开发效率提高 => redux-dev-tools + hot-reload
统一数据管理=> redux
性能提高 => immutable + purerender
路由控制器 => react-router(手Q暂时没采用)
为何咱们在优化的时候主要讲手Q呢?毕竟PC的性能在大部份状况下已经很好,在PC上一些存在的问题都被PC良好的性能掩盖下去。手机的性能不如PC,所以有更多有价值的东西深挖。开发的时候我就跟同事开玩笑说:“没作过手机web优化的都真很差意思说本身作过性能优化啊“。
我在《性能优化三部曲之一——构建篇》提出,“经过构建,咱们能够达成开发效率的提高,以及对项目最基本的优化”。在进行React重构优化的过程当中,构建对项目的优化做用必不可少。在本文暂时不赘述,我另外开辟了一篇《webpack使用优化(react篇)》进行具体论述。
在PC端使用Redux的时候,咱们都很喜欢使用Redux-Devtools来查看Redux触发的action,以及对应的数据变化。PC端使用的时候,咱们习惯摆在右边。但移动端的屏幕较少,所以家校群项目使用的时候放在底部,并且因为性能问题,咱们在constant里设一个debug参数,而后在chrome调试时打开,移动端非必须的时候关闭。不然,它会致使移动web的渲染比较低下。
这一部份算是重头戏吧。React做为View层的框架,已经经过vd帮助咱们解决重复渲染的问题。但vd是经过看数据的先后差别去判断是否要重复渲染的,但React并无帮助咱们去作这层比较。所以咱们须要使用一整套数据管理工具及对应的优化方法去达成。在这方法,咱们选择了Redux。
Redux整个数据流大致能够用下图来描述:
Redux这个框架的好处在于可以统一在本身定义的reducer函数里面去进行数据处理,在View层中只须要经过事件去处触发一些action就能够改变地应的数据,这样可以使数据处理和dom渲染更好地分离,而避免手动地去设置state。
在重构的时候,咱们倾向于将功能相似的数据归类到一块儿,并创建对应的reducer文件对数据进行处理。以下图,是手Q家校群布置页的数据结构。有些大型的SPA项目可能会将初始数据分开在不一样的reducer文件里,但这里咱们倾向于归到一个store文件,这样可以清晰地知道整个文件的数据结构,也符合Redux想统一管理数据的想法。而后数据的每一个层级与reducer文件都是一一对应的关系。
这套React + Redux的东西在PC家校群页面上用得很欢乐, 以致于不用怎么写shouldComponentUpdate都没遇到过什么性能问题。但放到移动端上,咱们在列表页重构的时候就立刻遇到卡顿的问题了。
什么缘由呢?是重复渲染致使的!!!!!!
说好的React vd能够减小重复渲染呢?!!!
请别忘记前提条件!!!!
你能够在每一个component的render里,放一个console.log("xxx component")。而后触发一个action,在优化以前,几乎所有的component都打出这个log,代表都重复渲染了。
更正:可见后面yeatszhang同窗的解释。
(网图,引用的文章太多以至于不知道哪篇才是出处)
上图是React的生命周期,还没熟悉的同窗能够去熟悉一下。由于其中的shouldComponentUpdate是优化的关键。React的重复渲染优化的核心其实就是在shouldComponentUpdate里面作数据比较。在优化以前,shouldComponentUpdate是默认返回true的,这致使任什么时候候触发任何的数据变化都会使component从新渲染。这必然会致使资源的浪费和性能的低下——你可能会感受比较原生的响应更慢。
这时你开始怀疑这世界——是否是Facebook在骗我。
当时遇到这个问题个人开始翻阅文档,也是在Facebook的Advanced Performance一节中找到答案:Immutablejs。这个框架已被吹了有一年多了吧,吹这些框架的人理解它的原理,但不必定实践过——由于做为一线移动端开发者,打开它的github主页看dist文件,50kb,我就已经打退堂鼓了。只是遇到了性能问题,咱们才再认真地去了解一遍。
Immutable这个的意思就是不可变,Immutablejs就是一个生成数据不可变的框架。一开始你并不理解不可变有什么用。最开始的时候Immutable这种数据结构是为了解决数据锁的问题,而对于js,就能够借用来解决先后数据比较的问题——由于同时Immutablejs还提供了很好的数据比较方法——Immutable.is()。小结一下就是:
Immutablejs自己就能生成不可变数据,这样就不须要开发者本身去作数据深拷贝,能够直接拿prevProps/prevState和nextProps/nextState来比较。
Immutable自己还提供了数据的比较方法,这样开发者也不用本身去写数据深比较的方法。
说到这里,已万事俱备了。那东风呢?咱们还欠的东风就是应该在哪里写这个比较。答案就是shouldComponentUpdate。这个生命周期会传入nextProps和nextState,能够跟component当前的props和state直接比较。这个就能够参考pure-render的作法,去重写shouldComponentUpdate,在里面写数据比较的逻辑。
其中一位同事polarjiang利用Immutablejs的is方法,参考pure-render-decorator写了一个immutable-pure-render-decorator。
那具体怎么使用immutable + pure-render呢?
对于immutable,咱们须要改写一下reducer functions里面的处理逻辑,一概换成Immutable的api。
至于pure-render,如果es5写法,能够用使mixin;如果es6/es7写法,须要使用decorator,在js的babel loader里面,新增plugins: [‘transform-decorators-legacy’]。其es6的写法是:
@pureRender export default class List extends Component { ... }
不从新渲染
你可能会想到Immutable能减小无谓的从新渲染,但可能没想过会致使页面不能正确地从新渲染。目前列表页在老师进入的时候是有2个tab的,tab的切换会让列表也切换。目前手Q的列表页学习PC的列表页,两个列表共用一套dom结构(由于除了做业布置者名字以外,两个列表如出一辙)。上了Immutablejs以后,当碰巧“我发布的“列表和”所有“列表开头的几个做业都是同一我的布置的时候,列表切换就不从新渲染了。
引入immutable和pureRender后,render里的JSX注意必定不要有一样的key(如两个列表,有重复的数据,此时以数据id来做为key就不太合适,应该要用数据id + 列表类型做为key),会形成不渲染新数据状况。列表页目前的处理办法是将key值换成id + listType。
(列表页两个列表的切换)
这样写除了保证在父元素那一层知晓数据(key值)不一样须要从新渲染以外,也保证了React底层渲染知道这是两组不一样的数据。在React源文件里有一个ReactChildReconciler.js主要是写children的渲染逻辑。其中的updateChildren里面有具体如何比较先后children,而后再决定是否要从新渲染。在比较的时候它调用了shouldUpdateReactComponent方法。咱们看到它有对key值作比较。在两个列表中有不一样的key,在数据类似的状况下,能保证二者切换的时候能从新渲染。
function shouldUpdateReactComponent(prevElement, nextElement) { var prevEmpty = prevElement === null || prevElement === false; var nextEmpty = nextElement === null || nextElement === false; if (prevEmpty || nextEmpty) { return prevEmpty === nextEmpty; } var prevType = typeof prevElement; var nextType = typeof nextElement; if (prevType === 'string' || prevType === 'number') { return nextType === 'string' || nextType === 'number'; } else { return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key; } }
Immutablejs太大了
上文也提到Immutablejs编译后的包也有50kb。对于PC端来讲可能无所谓,网速足够快,但对于移动端来讲压力就大了。有人写了个seamless-immutable,算是简易版的Immutablejs,只有2kb,只支持Object和Array。
但其实数据比较逻辑写起来也并不难,所以再去review代码的时候,我决定尝试本身写一个,也是这个决定让我发现了更多的奥秘。
针对React的这个数据比较的深比较deepCompare,要点有2个:
尽可能使传入的数据扁平化一点
比较的时候作一些限制,避免溢出栈
先上一下列表页的代码,以下图。这里当时是学习了PC家校群的作法,将component做为props传入。这里的<Scroll>封装的是滚动检测的逻辑,而<List>则是列表页的渲染,<Empty>是列表为空的时候展现的内容,<Loading>是列表底部加载的显示横条。
针对deepCompare的第1个要点,扁平化数据,咱们很明显就能定位出其中一个问题了。例如<Empty>,咱们传入了props.hw,这个props包括了两个列表的数据。但这样的结构就会是这样
props.hw = { listMine: [ {...}, {...}, ... ], listAll: [ {...}, {...}, ... ], }
但若是咱们提早在传入以前判断当前在哪一个列表,而后传入对应列表的数量,则会像这样:
props.hw = 20;
二者比较起来,显示是后者简单得多。
针对deepCompare第2点,限制比较的条件。首先让咱们想到的是比较的深度。通常而言,对于Object和Array数据,咱们都须要递归去进行比较,出于性能的考虑,咱们都会限制比较的深度。
除此以外,咱们回顾一下上面的代码,咱们将几个React component做为props传进去了,这会在shouldComponentUpdate里面显示出来。这些component的结构大概以下:
$$typeof // 类型 _owner // 父组件 _self: // 仅开发模式出现 _source: // 仅开发模式出现 _store // 仅开发模式出现 key // 组件的key属性值 props // 从传入的props ref // 组件的ref属性值 type 本组件ReactComponent
所以,针对component的比较,有一些是能够忽略的,例如$$typeof, _store, _self, _source, _owner。type这个比较复杂,能够比较,但仅限于咱们定好的比较深度。若是不作这些忽略,这个深比较将会比较消耗性能。关于这个deepCompare的代码,我放在了pure-render-deepCompare-decorator。
不过其实,将component看成props传入更为灵活,并且可以增长组件的复用性,但从上面看来,是比较消耗性能的。看了官方文档以后,咱们尝试换种写法,主要就是采用<Scroll>包裹<List>的作法,而后用this.props.children在<Scroll>里面渲染,并将<Empty>, <Loading>抽出来。
本觉得React可能会对children这个props有什么特殊处理,但它依然是将children看成props,传入shouldComponentUpdate,这就迫使父元素<Scroll>要去判断是否要从新渲染,进而跳到子无素<List>再去判断是否进一步进行渲染。
那<Scroll>究竟要不要去作这重判断呢?针对列表页这种状况,咱们以为能够暂时不作,因为<Scroll>包裹的元素很少,<Scroll>能够先重复渲染,而后再交由子元素<List>本身再去判断。这样咱们对pure-render-deepCompare-decorator要进行一些修改,当轮到props.children判断的时候,咱们要求父元素直接从新渲染,这样就能交给子元素去作下一步的处理。
若是<Scroll>包裹的只有<List>还好,若是还有像<Empty>, <Loading>甚至其它更多的子元素,那<Scroll>从新渲染会触发其它子元素去运算,判断本身是否要作从新渲染,这就形成了浪费。react的官方论坛上已经有人提出,React的将父子元素的重复渲染的决策都放在shouldComponentUpdate,可能致使了耦合Shouldcomponentupdate And Children。
lodash.merge能够解决大部份场景
(此段更新于2016年6月30日)因为immutable的大小问题一直萦绕头上,久久不得散去,所以再去找寻其它的方案。后面决定尝试一下lodash.merge,并用上以前本身写的pureRender。在渲染性能上还能够接受,在仅比immutable差一点点(后面会披露具体数据),但却带来了30kb的减包。
这里概括了一些其它性能优化的小Tips。
既然将数据主要交给了Redux来管理,那就尽可能使用Redux管理你的数据和状态state,除了少数状况外,别忘了shouldComponentUpdate也须要比较state。
Component的render里不动态bind方法,方法都在constructor里bind好,若是要动态传参,方法可以使用闭包返回一个最终可执行函数。如:showDelBtn(item) { return (e) => {}; }。若是每次都在render里面的jsx去bind这个方法,每次都要绑定会消耗性能。
传得太多,或者层次传得太深,都会加剧shouldComponentUpdate里面的数据比较负担,所以,也请慎用spread attributes()。
这个用法是工业聚在React讨论微信群里教会的,咱们能够将不怎么变更,或者不须要传入状态的component写成const element的形式,这样能加快这个element的初始渲染速度。
当项目变得更大规模与复杂的时候,咱们须要设计成SPA,这时路由管理就很是重要了,这使特定url参数可以对应一个页面。
PC家校群整个设计是一个中型的SPA,当js bundle太大的时候,须要拆分红几个小的bundle,进行异步加载。这时能够用到webpack的异步加载打包功能,require。
在重构手Q家校群布置页的时候,咱们有很多的浮层,列表有布置页内容主浮层、同步到多群浮层、科目管理浮层以及指定群成员浮层。这些彻底可使用react-router进行管理。可是因为当时一早使用了Immutablejs,js bundle已经比较大,咱们就不打算使用react-router了。但后面仍然发现包比重构前要大一些,所以为了保证首屏时间不慢于重构前,咱们但愿在不用react-router的状况下进行分包,其实也并不难,以下面2幅图:
首先在切换浮层方法里面,使用require.ensure,指定要加载哪一个包。
在setComponent方法里,将component存在state里面。
在父元素的渲染方法里,当state有值的时候,就会自动渲染加载回来的component。
目前只有列表页发布外网了,咱们比较了优化先后的首屏可交互时间,分别有18%和5.3%的提高。
更新于2016年7月2日
Android
React重构后初版,当时还没作任何的优化,发现平均FPS只有22(虽然Android的肉眼感觉不出来),然后面使用Immutable或者Lodash.merge都很是接近,能达到42或以上。而手机QQ可接受的FPS最少值是30FPS。所以使用Immutable和Lodash.merge的优化仍是至关明显的。
重构后初版
Immutable
Lodash.merge
iOS
在iOS上的fps差距尤其明显。重构后初版,拉了大概5屏以后,肉眼会有卡顿的感受,拉到了10屏以后,数据开始掉到了20多30。而Immutable和Lodash.merge则大部份时间保持在50fps以上,不少时候还能达到很是流畅的60fps。
重构后初版
Immutable
Lodash.merge
Chrome模拟器
用Chrome模拟器也能看出一些端倪。在Scripting方面,Immutable和Lodash.merge的耗时是最少的,约700多ms,而重构后的初版则须要1220ms。Lodash.merge在rendering和painting上则没占到优点,但Immutable则要比其它两个要少30% - 40%。因为测试的时候是在PC端,PC端的性能又极好,因此无论是肉眼,仍是数据,对于不是很复杂的需求,整体的渲染性能看不出很是明显的差距。
重构后初版
Immutable
Lodash.merge
从上面的数据看来,在移动端使用Immutable和Lodash.merge相对于不用,会有较大的性能优点,但Immutable相对于Lodash.merge在咱们需求情景下暂时没看出明显的优点,笔者估计多是因为项目数据规模不大,结构不复杂,所以Immutable的算法优点并无充分发挥出来。
测试注明
Android端测试FPS是使用了腾讯开发的GT随身调。而iOS则使用了Macbook里xCode自带的instrument中的animation功能。Chrome模拟器则使用了Chrome的timeline。测试的方式是匀速滚动列表,拉出数据进行渲染。
咱们在开发的过程当中,将上面所论述的内容,总结成一个基本的军规,铭记于心,就能够保证React应用的性能不至于太差。
提高级项目性能,请使用immutable(props、state、store)
请pure-render-decorator与immutablejs搭配使用
请慎用setState,因其容易致使从新渲染
谨慎将component看成props传入
请将方法的bind一概置于constructor
请只传递component须要的props,避免其它props变化致使从新渲染- - (慎用spread attributes)
请在你但愿发生从新渲染的dom上设置可被react识别的同级惟一key,不然react在某些状况可能不会从新渲染。
请尽可能使用const element
简单的tap事件,请使用react-tap-event-plugin 开发环境时,最好引入webpack的环境变量(仅在开发环境中初始化),在container中初始化。生产环境的时候,请将plugin跟react打包到一块儿(须要打包在一块儿才能正常使用,由于plugin对react有好多依赖),外链引入。
目前参考了这个项目的打包方案:https://github.com/hartmamt/r...
Facebook官方issue: https://github.com/facebook/r...
React-tap-event-plugin github: https://github.com/zilverline...
复杂的tap事件,建议使用tap component 家校群列表页的每一个做业的tap交互都比较复杂,出了普通的tap以外,还须要long tap和swipe。所以咱们只好本身封装了一个tap component
移动端请慎用redux-devtools,易形成卡顿
Webpack慎用devtools的inline-source-map模式 使用此模式会内联一大段便于定位bug的字符串,查错时能够开启,不是查错时建议关闭,不然开发时加载的包会很是大。
慎用太新的es6语法。 Object.assign等较新的类库避免在移动端上使用,会报错。 Object.assign目前使用object-assign包。或者使用babel-plugin-transform-object-assign插件。会转换成一个extends的函数:
var _extends = ...; _extends(a, b);
注意Object.assign是浅拷贝 Object.assign是浅拷贝,若数据结构层次较深的时候会拷贝失败,直接回传本来的object reference,此处推荐lodash.merge。
若有错误,请斧正!
PS: 要看效果得将一个QQ群组转换成家校群,可到此网址进行转换(手Q/PC均可以访问):http://qun.qq.com/homework/。
转换以后,能够经过QQ群的加号面板,或者群资料卡进入。
更多精彩内容欢迎关注bugly的微信公众帐号: