社交网络日新月异,需要更快,更灵活的技术架构才能满足用户日益丰富的媒体需求,Hybrid App架构具备了快速发布的能力,同时也有更接近Native的流畅体验以及性能;本视频来自ArchSummit深圳2016腾讯社交平台高级工程师黄俊涛的分享演讲,主要介绍QQ空间Hybrid架构,包括如何提高H5页的加载速度以及稳定性,以及使用React Native如何提高体验,改善性能。
黄俊涛:大家下午好!我是来自腾讯QQ空间的黄俊涛,我在腾讯英文ID叫Shine,所以大家其实叫我“Shine(帅)哥”就可以了。我是2011年的时候加入腾讯的,之前是做朋友网、QQ空间PC上一些网站Web前端的开发工作,2014年中间有一段时间还负责过全民K歌Android的开发,现在主要的精力是投入到QQ空间App内的H5架构相关的一些工作。
这次很高兴来到这里和各位互联网界的技术同学作一个技术交流,今天我主要讲的主题是《QQ空间面向移动时代Hybrid架构设计》。说到QQ空间,很多人第一印象就是想起以前在QQ农场在空间里面偷菜那段欢乐时光,相信在座也有人在空间中转发过日志,以为自己的QQ等级可以加一个太阳,这些种种其实就构成了我们一个QQ空间的印象。QQ空间作为第一代的社交网络至今发展到现在已经有12年了,它不仅记录我们年轻时候的美好青春,而且还继续承载着下一代年轻人的生活记忆。
言归正传,这次我会分享QQ空间H5团队在过去一年里面我们正在做的一些事情,简单来讲就是两个部分:H5在QQ空间App里的架构设计和React Native动态化方案在App内的实践情况。
一、H5在QQ空间App里的架构设计
先让我们看一下QQ空间H5的应用场景,它主要作为我们手机QQ和手机QQ空间的二级页(Tab页)的入口,简单举个例子说明H5承载的场景:比如手机QQ里面空间的留言入口、访客入口还有个性化的相关入口(背景Cover之类),这些功能模块已经被我们切成H5了。
这里有必要说明为什么我们把它们切成H5,H5作为这么久以来比较常规动态运营的方案,可以解放你的业务特性发展,而且关键是我们手机App迭代周期相对比较长,可能需要一两个月才有一个新的版本发布,这时候我们还是需要可以动态化运营的H5作为承载。当然这些二级入口有数千万甚至上亿的访问量,可以说是国内访问量最大的H5页面之一,但是在我们做完这些功能或者业务模块切换以后,我们的挑战就已经来了,主要问题还是来自于用户体验上的差别,比如我们的留言板做完H5(即把Native切换成H5之后),它的用户活跃度甚至一度下降了10%,这是很可怕的事情。
大家最直观的反馈是H5没有Native好,那么我们来看一下Native可能比H5好的地方在哪里呢?说到这一点大家第一反应肯定是:Native快。这是我们大家的普遍共识,现在通过一组视频的对比看看Native究竟快在哪里。
第一段视频是展示Native模块首次加载、二次加载以及杀掉App以后再重新进入的加载过程,用的是我们空间的说说模块(该演示视频已整合至尾部视频第1部分),大家可以留意一下首次加载的时候,用户点击说说Tab,因为是3G模式可能会比较慢,所以大家会先看到一个灰屏,转了很久菊花以后会加载首次出来的内容,而这个时候你看到的内容还是比较完整的;
我们再看第二次进入的情况:非常流畅。因为我们前面已经加载进来一次了,所以会流畅一些,有个缓存的机制在里面;
然后我们杀掉App之后再重进以后再看,也是比较流畅的体验,可以说是丝般顺滑。
然后我们来看一下我们另一个H5对比,这里用的是跟它同级的模块——留言板作为对比,我这里用一个传统的H5容器去做一个加载,点击了H5链接的场景(该演示视频已整合至尾部视频第2部分),大家可以看到,首次加载这里和Native是一样的,也有一个白屏或者灰屏的现象出现,我们再看第二次进来的情况,大家可能有一个感觉:怎么第二次比第一次还慢?其实这里就是H5的一个传统的通病,HTTP的协议不一定很稳定,它不能够保证第二次进来就一定比第一次好,所以从加载速度来看Native比H5好在两个地方:
首次加载速度可能相对比较快,因为Native网络层可能更先进,代码也是在本地去运行的,给用户的体验是:先出来东西给用户去看,用户就觉得很好很完美;
二次加载体验机制可以完爆H5,这里主要原因其实我们也看到就是缓存的逻辑。
除了速度方面Native比较好的优势是相对比较稳定,这体现在几个方面:
运营商的劫持,H5可能存在小广告这样的问题;
体验一致性的问题,比如大家用手Q或者微信,在拉动态的时候是经常可以成功拉到的,但是如果有人分享了H5链接给你,你打开可能要等很久才能看,就有很久的白屏体验,而且有时候甚至你再重新进去的时候可能看不到这个界面。
H5还会经常会遇到这个问题,我这里做了一个视频(该演示视频已整合至尾部视频第3部分),这时候大家可以看到它的页面框架已经出来了,但是有一个很危险的事情是你的页面出来了,但是你的交互或者用户操作是没办法响应的,这跟JS加载成功率还有代码模块下载的问题有关。
怎么去解决上面所说的这些问题呢?很多人说改回Native就好了,但是H5的这个改造已经箭在弦上不得不发了。我们当时的预期是希望可以在速度和稳定性上对H5做提高和改造,目标是让大家感觉不出来你访问的业务模块是H5还是Native。
当时我们也有做过一些技术选型和预研,业界也有一些解决方案有人分享过,比如这里所说的离线包+预加载,我们去年也有做过类似的实践,但是采用完以后总觉得有点别扭,对于我们开发者来说反正就两个字:不爽。我们总结一下这个不爽在哪里呢,有三个限制:
离线包+预加载这个架构主要解决的还是静态资源的问题(静态HTML、静态JS、静态CSS它们的加载、它们离线的东西),因为我们本身QQ空间H5架构,它的页面是后端(Node端)去构建的,所以这里存在冲突的问题,而且这个也是由我们业务UCG架构决定的;
比如留言板,不同用户的留言板内容肯定是不一样的,如果没有这个数据的差异,这个留言板页面没有必要展示出来,因为它一定需要用户真正的数据存储才有意义,它不像传统的运营活动页或推广页那样所有人看的都是同一个Banner图或同一个页面,这是第一个问题;
第二个问题,我觉得离线包比较危险的点在于它比较影响我们的发布效率,因为它需要二次发布,发布完了可能还要打包,然后再去离线包平台那里去发布,而且关键你还要有一个离线包这样的平台,其实是比较影响效率的。而且最大的问题在于当你的业务扩张的时候,比如之前有人分享过,他们的离线包有100多个的时候怎么去管理、怎么按优先级管理,可能有一些业务例如节日运营时,你可以提前把这100多个包埋进去,但是在日常运营里面你很难去这么做,比如一个新用户进来,你不能他一进来你就把100多个离线包给他推送过去,这很损耗用户体验;
第三个问题也是我个人觉得最关键的问题,它的更新覆盖率总是没有办法到达100%,离线包的更新是异步的过程,你没有办法保证用户进入的时候一定是更新了你最新的离线包,不能保证是用户的版本是你希望的版本。
综上所述,基于上面种种限制我们还是没有采用这个离线包+预加载的方案。回到问题本身,其实从速度方面我认为H5要赶超Native速度主要有两个点:
首次进入的时候如何去提速;
有缓存的情况(例如你以前进入过了),怎么实现秒开的体验,马上能够出来内容。
我们看一下传统H5的加载流程,大家可以看到这是一个比较典型的串型加载流程,用户点击你的业务模块的时候,它其实需要先去启动你的WebView,WebView再去loadUrl把你的HTML请求(HTTP/HTTPS)发到服务器端,然后再把HTML返回来再设置进WebView里面。
这里有两个问题:
WebView的耗时
根据我们手Q的统计数据,Android的WebView平均打开启动的速度耗时是0.8秒,有些Android机型基于系统环境可能需要一点多或者两秒左右;
HTTP协议的网络层
HTTP有DNS或者建立链接的耗时,HTTPS有建立SSL链接的耗时。
所以H5通常带给用户的体验是:我点进来不管是怎么时候,是第一次第二次还是第三次,我点进来先等两秒再说,看两秒白屏后页面才可以加载进来,其实这里我们只做了两个事情去优化首次加载流程:
把WebView启动和发送请求改成并行
客户端在启动WebView的时候同时发起HTML的页面请求,这里可以实现一个效果:本来我的耗时是T1+T2,现在为T1或T2的最大值,这样达到并行加载的效果。
当然这里还有一个优化的空间,我们其实已经由客户端接管了发送网络请求的事情,以前接管的时候是用HTTP协议,比如像iOS用的是URLSession模块,但已经让客户端接管了,为什么不能够更大胆一些呢?
Socket通道
其实我们可以不拘泥于数据传输的方式,所以这里我们把一个HTML的加载从HTTP/HTTPS改造成了一个Socket通道,这是一个App里面长连接的管道,大家可以这样理解为手机QQ发消息的那个通道,我们是用这个长连接的通道来做HTML的传输的,它的作用在于只要你的手机QQ能够收发消息,你的HTML页面就能够加载进来,这样就达到体验上的一致性,同时网络也有比较稳定的提升。
我们基本上做了两个事情就达到首次进入加速的目的,但后面才是一个关键:首次进入我们已经做过优化了,但二次进入怎么去做优化去提高它的显示速度呢?
我们第一个想到的是做整个HTML页面的离线缓存,我们在这里加了一层Cache层,它会保存用它加载过的HTML页面,WebView启动完后如果Cache层里有昨天访问过的HTML就先把它设置进去,让WebView先有内容显示,最大化打开速度。当这个时候有一个新的内容过来的时候再把这个新的页面刷新一遍,这样跟Native的体验是一致的,就跟我们看到的视频Demo一样:进入说说的时候第一次可能要等一会,第二次进入时马上可以出来,而且上面有转菊花,如果这时候有更新的话也会被刷掉。
当然这里除了HTML缓存以外,我们还需要二次更新或者是缓存管理的逻辑,这里我们想到一个传统的方案:ETag。我在12年的时候就做过ETag的实践调研,它在传统PC浏览器上兼容性不好,很少浏览器可以支持。我们在App内做了一个升级版的ETag,它的ETag标记是用我们的HTML页面的内容用SHA-1算法算成32位的标记,然后从客户端带到服务器端,服务器端就可以识别你的内容是否发生了变更,它也是为了我们的304服务的。因为我们知道,用304和ETag可以很好地减少Web应用带宽和负载的压力。
我们还新增了一个叫Cache-Offline的字段,是和ETag同级的,它的作用是什么呢?就是作为一个二次更新的策略标记,用于标记你的服务器端返回的HTML内容是否进入了你的缓存,因为可能会有一些错误的页面的情况,它的意思就是控制你的内容是否进入离线,还有是否要重新刷新一次WebView。
刚才说了三个东西:一个是并行加载优化,第二个是HTML的高速通道(Socket通道),第三个是离线缓存机制,我们看一下演示效果(该演示视频已整合至尾部视频第4部分),这个应该是3G网络下,即网络传输速度比较慢的场景去做的模拟,这样才能看得出来优化的效果,如果大家用Wi-Fi的话首次和二次加载都会比较快。我们看一下首次加载还是比较慢,白屏也比较多,上面有进度条一直加载,这其实是异步的JS在加载,但是我们可以看到首次可以实现相对来说比HTTP更快地加载。接下来我们看二次进入的速度,这次当然也有一个WebView缓存的情况出现,因为我们图片和JS都是缓存得来的,但是可以看到HTML这个内容是马上加载完毕并且显示给大家看的。
这时候我们做一个页面的更新(在这里我在留言板回复了一句话),让大家更好理解我们二次更新是什么样,我退出来再重新进入留言板,大家可以看到刚看到的界面是之前访问过没有那条回复的界面,但是过一点时间马上会刷新为有那条回复的界面,这样跟Native的交互体验比较像了,当然这里可能还有一个瓶颈在于你这个二次加载的秒开还依赖于你的WebView启动速度以及页面layout布局耗时,这是我们刚刚所做优化的雏形图:
并行加载;
HTML改成Socket通道去传输;
WebView的HTML页面缓存管理;
这些改造上线后用户体验和反馈都很好,从我们前端测速表现来看是也是杠杠的,大家关注图中Clickstart的测速点,Clickstart是从用户点击到看到内容的一个计算时间,大家可以看到如果用HTTP的话(白色那一行)它的耗时比较长,需要3、4秒;大家再关注绿色这一行,如果用了并行加载和Socket通道以后它的测速提升是非常明显的,iOS和Android都有一个比较好的提升;
我们再来关注一下缓存命中的情况有多快,即我们从视频看到的二次进入情况,因为这个基本上本地已经有一个缓存的HTML了,你要显示出来基本上只要WebView的耗时,只要你的WebView耗时完成了,然后我们只需要本地IO读取HTML文件就可以显示出来,所以红色这一栏基本可以实现秒开的体验。
除了这个测速数据我们还统计了离线缓存率,首先刚才说的304这种情况对于用户来说是最好的,因为基本上只需要很少的网络传输,我告诉你你不用更新你的本地缓存,直接用本地缓存显示就最好了,这样流量基本为零,也不会有二次更新的逻辑。
即使需要二次更新时也还好,我二次进来还是可以达到秒开,还是可以看到内容,但可能要过一会才能刷成最新的内容,这个比例是40%多。二次更新和304加起来是50%多,剩下的就是首次加载(本地什么都没有就要第一次加载的情况),这个比例现在看来还比较高,但其实这个跟业务特性有关,我们的留言板业务特性是用户经常发生变化的原因;比如我们的背景Cover的话,它的304比例可以到达百分之八九十,对你的Web应用带宽和流量损耗降到最低。
看到这里,我个人认为这里还能够做更多事情,其实我比较纠结二次更新这里,因为它占的比重比较大(40%以上),但它又很别扭,因为它每次更新都需要更新18KB网络包的传输体积,这些不一定每次都需要传输的,我们页面是直出CSS的(CSS是合并到HTML里面的),这些内容我个人认为二次更新根本不需要传输,这样才能达到二次更新时网络层的最优化。
这里我们分析了我们页面结构,看一下有没有可能做到二次进来时每次只传输数据的层面,我们通过页面结构分析得出页面主要由三个部分组成:
我们直出的CSS;
用户UGC的内容(每个人这一块都不一样);
还有一些其它节点比如内置的集成JS等等其它代码模块;
这就给我们带来一个问题,其实我们这里本来想设计一个增量更新的逻辑去做H5的二次更新来达到省流量的目的,但H5页面要做增量更新的话不像静态HTML做一个增量更新这么容易,因为如果是静态页面做增量更新很简单:本地有一个缓存,我只要计算它们之间的差异把差异发过来就行了。但是如果是一个HTML直出页面的话,它每个人的内容都不一样的,我不可能把所有人的HTML页面内容都保存到本地,这是不收敛的情况。
所以这里H5要做一个增量更新只有一种可能,就是接下来说的的动静分离方案,我们把用户的页面动态的部分挖出来。比如像留言板,我们把页面的留言列表挖出来;页面的标题也挖出来,因为这可能是跟个人用户信息相关的内容。由这些内容组成Data层(由title和大的body组成),这样就可以保持我们中间静态模板的纯粹性,这个静态模板对于所有用户来看的话都是一样一致的,正是这个静态模板的存在我们也可以实现H5的增量更新。
大概的做法是:我们的静态模板也跟ETag一样有一个Template-tag的结构,它是我们的静态模板用的MD5算法算出来的32位的值,它也会跟随客户端发给服务器端,服务器端也会返回客户端,由服务器端去做增量的差异对比。比如静态模板这时候变化了,我们就会把这两个文件作一个对比,用BSDiff算法来算出来它的差异,然后用二进制结构把这个结构再丢给客户端,由客户端再重新把这个差异、本地缓存的静态模板还有数据合并出来成一个HTML,这样就可以还原成一个HTML显示出来给用户看。
通过H5的增量更新我们实现了40%二次更新的页面比例,由18KB减少到只需要传递8KB的动态数据和模板的差异就可以实现二次更新了。
说到这里增量更新已经说完了,但是我们还统计了一个比较有意思的数据,我们在服务器端(Node层)上报了页面的PV,同时我们在浏览器端(客户端)也做了PV这样的上报,发现一个很有意思的是:到了用户页面的转化率不高,即异步上报和我直出上报的比例不一致,转化率只有93%。但这里我们要怎么办呢?这个问题主要有两个原因导致的:
页面静态的JS加载成功率不高;
网络的损耗,可能你的HTTP请求发出去直接被block掉或者一些网络变化导致它回不来。
其实这个跟刚刚我们视频演示的进了一个页面看到页面结构但点击没有反应,跟这个情况类似或者一样的,我们能够做一些什么呢?
我能想到的,就是我们要提高页面可用率,页面结构只要下来了我一定要保证是可用的而且可以点击的;同时我们还要实现一个静态资源的缓存复用,大家看视频也可以看出来我们的静态资源缓存非常依赖于WebView,WebView的缓存没了我们静态的缓存也没了。
我们首先看一下传统的HTML是怎么加载JS的,主要就是两个思路:
把JS内置到我们的HTML里面
这种内置把所有的JS打包到HTML里面是可以保证100%可用率,你只要看到页面了,肯定是JS执行完了也可以点了,然后通过我们前面所说的增量更新,我们也可以控制它二次传输的时候流量的情况,但这里的问题在于如果我们把所有的JS都打包到一个HTML页面去下载的话,有两个问题:
首次传输加载体积太大了
我们传统JS库像React、Angular,还有我们以前的 jQuery都比较大,有几十KB到100KB,整个打包下来一个页面甚至可以到达100多KB的大小,和前面所看到的18KB非常不一致,而且这样加载也会慢很多;
公共JS像我前面说到一些框架不能实现复用
比如留言板是H5的,它的公用JS只对它这个页面是复用的,因为有增量更新的存在,但对于别的H5页面,比如我们的背景Cover、我的签到,这些H5页面是没有办法跟它共用一份公共JS缓存的。所以这里带来一个问题:异步加载会存在一个损害率,还有前面说到安全的问题。
我们实现方式是公共JS(像我刚才说的框架文件)通过HTTPS/Socket通道去加载
和之前解释的高速通道是一致的,然后通过offline参数缓存到本地,我们这个公共库文件更新的时候会有一个文件名的变更来实现缓存复用;不支持HTTP Hook的情况我们还是会走HTTPS加载。
业务相关的JS还是会打包到HTML里面跟随Socket通道一起传输,业务JS一般也不大,不到10K左右,当然你也可以选择不内置,可以走之前所说的HTTP通道或Socket通道去加载。
这样就实现了静态资源的安全加载和缓存复用,有了这些之后我们甚至也可以具备一些跟Native离线相媲美的交互,下面我们可以看一下视频(该演示视频已整合至尾部视频第5部分),大家可以看到网络正常的情况下,页面结构出来了我的点击也是没有问题的,这时候我退出去杀掉App(现在逻辑是杀掉App,WebView缓存就会被清掉),大家可以看到我们之前所演示的情况(它会出来但可能点击有问题),但用了我们这个方案重新进入以后,虽然网络有差异,但其实可以看到我们的点击操作是有效的,这样就相当于把代码模块以类似插件的形式植入到App里面,实现了离线交互这样一个操作功能。
说到这里,前面说的基本都是把HTML、CSS、JS文件通过我们新的传输通道或者一些新的东西缓存到本地达到一个加速的功能。这里还有一个遗漏的东西:除了静态资源和页面以外,我们还有异步ajax接口,这里也可以采用同样的方案,我们最后做的事情把异步读写接口也接入了我们的Socket通道,这个带来的意义主要在于两个:
实现了网络层的全部托管
接口的成功率和速度也有一定的提升,而且也跟Native做到一样的体验。因为用的是稳定的通道,只要你的QQ聊天消息或者拉取动态的Native接口可用,我的这个页面就可用;
保护我们的手机H5异步接口
因为社交网络的一些UGC内容价值大会出现一个情况:它经常会被刷。有可能是节日运营带来的刷,经过我们分析也有很多时候是坏人来刷我们,他们盗取了很多用户网页的恶意登录态票据不停地批量攻击,比如说我们写留言,他们可能会对某个号码做一个轰炸的攻击,同时有上10万人同时对那个用户的留言做这样的写操作。
其实从图上也可以看到这些坏人的流量情况,但是我们做了Socket层的改造以后,我们就可以分离开来:
从H5 Socket通道过来的异步接口我们可以更加信赖一点,我们可以给你提供差异性的服务,他们对于H5正常操作是比较稳定的;
但是对于那些还是以前历史包袱带来的H5接口,我们可以采取更严格的安全策略或安全模式来实现H5异步接口防刷的功能。
最后是H5的架构图,其实总的来说就是两到三个东西:
实现了H5网络层的全部替换(接管),由HTTP/HTTPS替换成Socket;
实现了缓存机制;
通过增量更新的模块和逻辑实现了代码模块的离线化,降低网络消耗和优化了用户的体验;
这是目前QQ空间H5的整体架构:
通过前面的演示视频大家可以看到,用户H5体验已经提升了一个level了,但是跟Native对比H5还有很多方面的差距,比如说标准组件和基础能力,举个例子像我们的Video标签,其实兼容性一直都不是很好,还有一些逻辑实现还有一些问题;而且我们没有Native的ListView这样的组件;包括我们的操作流畅度和灵敏度,还有内存等性能指标,这些WebView跟Native还是有很大的差距。
当然我们还是可以看到WebView在这块有变强的趋势:比如我们腾讯内部的QQ浏览器有自研的WebView内核,我们也是自己采用接入并且对它有一些优化;iOS上的话我们也可以看到WKWebView其实也是有一个比较好的优化的,比如页面加载的内存损耗已经变小很多。
但总的来说我认为WebView现在发展还是处于比较缓慢的过程,虽然未来WebView我们可以想象到有很多特性,但是就现在来看WebView不足以我们去达到极致的用户体验。我们就在想,有没有可能从WebView或者从Native层去做一个替换呢?比如能不能用Native的View来替换掉视图层组件,但又不失灵活性呢?
二、React Native动态化方案在App内的实践情况
通过一些研究和对一些方案进行选型,我们试了一下React Native(以下简称RN),RN是这样的一个壳(由JavaScript语法编写):允许你去创建原生组件,也符合我们对视图层的要求。其实它和WebView很像,它也有一个JSC(JavaScriptCore),类似Chrome的V8实现了基于Flexbox的布局引擎,类似Chromium的WebView和Firefox的Gecko。
先介绍一下目前我们腾讯RN的实践情况,目前我们手机QQ有留言板已经是在做RN的改造;我们手机QQ空间也有话题圈和情侣空间这两个模块也做了RN的切换灰度,目前日访问量数以百万计;还有我们的兄弟部门QQ音乐还有全民K歌,他们也在RN实践上取得好的效果。
先来介绍一下我们RN改造的场景,我们选取了两个重点业务——话题圈和留言板来做这个RN改造,我们可以看到要实现这个RN改造我们的原生组件需要一些小东西:TextView,Image,ListView等这种RN框架已经提供了的组件。我们可能还需要Video的组件,但本身框架不提供这个组件,所以就要自定义去实现;还有图文混排的组件,它原生的Text组件不支持这种图文混排的情况,原生的Text组件可能还有一些兼容性的问题,比如一些机型会字体缺失或者不见某个字的情况。
先来看我们怎么去做RN自定义组件的,其实很简单,iOS和Android差异不大,继承RCTViewManager,通过RCT_EXPORT_MODULE把模块暴露出来,模块还要对接相应的接口,暴露需要提供给前端调用的一些属性和方法就可以了。这样在前端就基本可以实现调用具有Native能力的自定义组件,我们前端可以通过引入这个模块,就像引用普通节点一样就可以去调用QzVideoView。
我们通过视频看看这个Video组件效果:(该演示视频已整合至尾部视频第6部分),这里有一个自动播放,当然H5也可以做到,但可能H5的Video兼容性有些问题,接下来点击Video到Native的浮层,需要说明RN Video的自定义标签和浮层的Video的缓存管理是一样的,所以它可以做到无缝切换续播的能力,因为这里的Video加载和缓存管理其实是同一个地方。
到了现在我们自定义组件已经上基本完成了(Video),下面我们还需要一个容器,这个容器我们选了原生提供的ListView容器,这个容器作为RN框架提供的重要容器,但是我个人认为它的性能实在让我大跌眼镜或者说差强人意:
首先在于它的滑动刷新率、滑动性能,如果你不开启removeClippedSubviews属性,在iOS上它的显示树就会把所有列表项都显示出来,这个是可以通过iOS Xcode用调试工具打印出来确认的。当然如果你开启的话,可以把屏幕外的节点不在显示树上显示去实现一个滑动性能的优化,但这个不是最关键的问题。最关键的问题是虽然这里不把它放在显示树,但这个节点还是在内存里,这里就带来一个更严重的问题:当你的ListView无限下拉无限滚动的时候,内存的无限增长的情况怎么去解决?
从图可以看到我们下拉了200个ListView的节点,内存就到了300MB,我们的iPhone5只有512MB的内存,所以很容易出现内存的问题。
针对这个问题,实话实说没有比较好的解决方案,在RN里面也没有提供比较好的解决方案,但是我们可以基于它本身做一些自己的优化:比如说我们Android的RN上面,在RN15的版本其实做了这样的替换,以前的ListView组件映射的是原生的ScrollView组件,到了15的版本把这个ScrollView换成了我们更好的RecyclerView的容器组件,这个容器组件就可以相当于把不在屏幕外的节点实现一个置空,就是说把里面的内容置空达到一个内存可回收的作用,但内存也不是完全回收,因为置空以后还是有空的View存在,但对比以前那种内存全部占用已经好很多。
但在iOS上就没有这个么好的组件,因为客户端那边没有提供类似的组件,而是对接一个UITableView实现可复用内存可回收的原生组件,耗时比较大,其实我们也做过一个,但是灵活性上会有一些问题,大家可以尝试把UITableView组件作为自定义组件去切换迁移。
我们这里采用一个比较简单的做法,在前端用JS做一个手动的回收,把屏幕外的节点用一个宽度为1的空View作为一个占位,这样其实就可以达到内存稍微增长没有这么厉害而不至于达到无限制的增长。
从这个图可以看到这个内存增长已经没有那么崎岖了,但是实际上这个问题还是没有根本解决,我们也期待RN在未来30的版本有Window 的ListView之类的原生组件可以提供给我们原生参考使用。
做完这个容器组件以后我们已经迫不及待地看它的启动速度了,前面H5已经讲了怎么去加速,怎么让用户最快看到内容,但是我们改造完RN以后发现,如果你不做任何优化,那RN这个加载速度比H5慢太多了,如果不做优化加起来的耗时能达到4、5秒,我总结一下这里有五个优化:
RN的上下文,可能耗时1秒左右,所以这里只是对某个业务模块进行切换迁移的话一定要预加载或做一个常驻;
jsbundle不能放在远程,一定要本地内置一个,然后有一个异步更新的逻辑去更新jsbundle;
执行jsbundle这里学问比较大,因为jsbundle分两个部分:
公共框架的部分,比如建立模块与Java层,即原生层的一个映射;
还有一层是我们自己的代码,就是UI逻辑的渲染
所以这里需要做分包,把公共的jsbundle分离出来,去做一个预执行,这里可以节省600毫秒左右的时间;
还有数据加载,这个就老生常谈了,数据结构一定要缓存到本地一份;
最后是我们业务逻辑即UI渲染这部分,要去做首屏组件的优化,组件层不能做深度嵌套,减少首屏可见的节点个数。
通过以上所有这些优化,我们可以看到跟之前我们用了那套先进的H5加载逻辑对比,在Android上提升很明显,因为Android上WebView实在太慢了,但在iOS上去做提升就见仁见智吧,我觉得也有一定的提升。
最后我们来关注一下RN框架的Crash,主要在Android上有两个Crash一定要在源码上注释掉:
AssertError;
属性变化的Crash
这两个Crash加起来的占比是0.5%左右,还是比较大的,还有在iOS上有一个imageLoader Crash,这个Crash简单说下就是RtcImgloader有一个逻辑本地缓存时,没有超过200兆就会写本地缓存,如果这个图片本地缓存它的response为Null的话它就会Crash掉,这个占比是在0.05%。
我们最后看一下版本兼容性问题,其实我个人觉得还好,比如Android,Android4.1以下其实在我们App统计已经很少,大概占3%-5%左右,但是我们还是用了一套H5页面作为降级方案,这样可以给RN一个比较好的灰度缓冲的时间,万一上线有问题我们可以快速切换。
总的来看,RN框架这个成熟度目前来看还是稍显不足,比如RN26版本到我们现在30版本,每一次升级都会带来框架接口变化,相当于每一次升级你自己的代码都要改很多,还有前面说的Crash的迁移,但是往好的方向看的话,我个人认为RN生态还是比较好的,算是生机勃勃的情况,而且两周有一个迭代的版本,动态化方案借助我们RN也有机会成为解放UI视图层方案的标准,如果等哪天它长大了,比如像可以解决前面说的一个ListView item内存reuse的问题,或者提高它的体验上线,我认为RN发展有更好的一天,当然也期待它1.0那一天。
最后跟大家分享一下最近遇到的事情,前段时间我跟InfoQ合作过一个直播节目,叫大咖说,直播完之后就有很多人找到我交流一些前端相关、技术相关的还有技术成长相关的内容,有一个问题我比较印象深刻,在这里也说一下,问题是这样的:我是一个Web前端的工程师,每天工作都是单一重复前端UI和动画相关的工作,比较枯燥,感觉自己成长会很慢,这时候该如何找到自身的价值呢?
其实我认为现在这个时代是我们Web前端的幸运,JavaScript提供了很好的土壤,可以用来编写NodeJS、页面甚至App,如果三块都搞定的话甚至可以称为全栈工程师。大家可以看到我前面所分享的内容,绝对不是我一个人或者说一个端就能够实现或者做完这一套的,我们客户端同学会和一直帮我们改造优化WebView、提供RN的框架,NodeJS提供H5页面的成长还有增量更新的能力,JavaScript还有CSS来编写我们Web层还有App层整个UI界面的渲染,可以看到我们整个技术团队的紧密合作和努力。
至于自身价值而言,我认为自己无法给自己去给做一个估值,与其追求自身价值不如更多地去追求团队价值和用户价值,比如前面说的H5页面进入怎么让用户最快、100%可用、还有流量的节省,其实团队和用户会因你为他们做的事情而为你自己定了价。
移动化时代其实需要我们学会变通和快速适应,我个人认为不要拘泥于我们Web前端,而是放眼整个大前端,说不定下一个虚拟化时代就到来了,也需要我们前端同学的一起努力。这需要我们具备扎实的计算机基础、宽广的技术视野,还有我认为最重要的举一反三的学习能力和可以全局看问题的眼光,这样的话我认为前端同学走到哪里都是可以具备高价值的。
因为那位同学可能今天在现场,所以我在这里跟大家唠嗑一下,最后一句话收尾,也是我的讲师海报上的一句话,但是没有贴出来:“程序员还是要对自己好一点”,谢谢大家。