本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/mUzq4NaBHeF3T-NStLzuRw

作者:peterlmeng

导语

最近特别火的狼人杀和最近特别火的React Native会擦出什么样的火花呢?本文和您一同探讨RN性能优化的现实场景。

项目简介:

狼人杀游戏是多人实时性游戏,对流畅度等性能都有要求。作为大型游戏,无论从代码规模和迭代速度来看,手Q的安装包和版本迭代速度都无法用native来承载这样的游戏。从而React Native成为了比较好的选择。

手Q React Native 简介

在手Q目前使用的React Native 版本是0.15版本。下面的数据分析都是基于手QRN0.15版本进行的分析数据。

问题分析

开发过React Native的同学,大体都对白屏界面有所了解。作为RN原生自带功能,基本上每个使用RN的业务都在优化这一阶段。通过对狼人杀的测试来看,首次从RN启动到渲染,耗时基本有1.7s左右。而这些耗时数据还是在iPhone6s中测试得出,可想低端局的情况可能会更加糟糕。

分析性能

工欲善其事必先利其器,要分析其耗时。还得从源头着手,根据常规做法,都会将React Native打包的js拆分成Base Bundle和业务Bundle。从上图,RN 加载流程来看,加载BaseBundle与业务Bundle的耗时是可以有优化空间的。


优化的方案和大多数人的思路一样,只需在业务启动前预加载BaseBundle与业务Bundle即可达到优化时间的效果。

目前所遇到的瓶颈


在优化的开始,我们可能一直把精力放在BaseBundle中,认为BaseBundle是RN的公共库,体积肯定不小。但是从数据来看,我们的狼人杀业务Bundle已经是1.8MB(纯js代码,不包括资源文件)而BaseBundle只有918KB,已经是两倍的体量。现在还只是狼人杀业务的初期,随着业务的快速迭代,业务Bundle只会更快的增加。而过大的业务Bundle所导致的加载时间也会加长。

可能有同学会说,这不是有预加载嘛。我承认,预加载确实解决了绝大部分业务Bundle的加载耗时。但是,并不是每次预加载都可以刚刚好预加载好业务Bundle。虽然业务Bundle加载耗时变长,预加载好的几率就会慢慢变低。

而这不是最关键的行为,最关键的是内存的消耗,我们来看一张图

从上图就可以看出,仅仅是BaseBundle,仅仅只是在内存中展开,还没有到运行。这个时候内存消耗已经达到了6MB。而整个狼人杀RN渲染起来,则消耗了20MB以上的内存。而这还没有包括业务使用的内存。在手Q中,内存的消耗是巨大的,而留给狼人杀使用的内存其实已经很少了。从这里可以看出,内存的优化好像更加迫在眉睫。

React Native 按需加载

React Native的思路是在业务运行之前,将所有js代码在JavaScriptContext中展开。这个逻辑本身没有什么问题。但是,我们需要改造成按需加载。按需加载的本质就是将不是关键路径的业务RN拆分开,变成插件中的插件。当业务触发到此逻辑的时候,再去将js代码动态展开。达到动态执行的目的。

而我们想要达成按需加载的效果,可能会面临着三个挑战。

  • 1.js在动态运行的时候,代码注入的问题。

  • 2.js模块与模块之间相互引用的问题。

  • 3.打包工具改造的问题。我们来依次看下这三个问题。

动态注入


1.从JS层面分析,想要达到JS代码的动态注入。必须要和运行的JS在相同运用域下面。我们通过分析打包后的JS代码得知,必须要在__d(verboseName + 模块名称)作用域下面。

2.从native层面分析,想要达到JS代码的动态注入。则必须要拿到JavaScriptCore中的JSContext。

- (void)enqueueApplicationScript:(NSData *)scripturl:(NSURL *)urlonComplete:(RCTJavaScriptCompleteBlock)onComplete
{RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");RCTProfileBeginFlowEvent();[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {RCTProfileEndFlowEvent();RCTAssertJSThread();if (scriptLoadError) {onComplete(scriptLoadError);return;}RCTProfileBeginEvent(0, @"FetchApplicationScriptCallbacks", nil);[_javaScriptExecutor executeJSCall:@"BatchedBridge"method:@"flushedQueue"arguments:@[]callback:^(id json, NSError *error){RCTProfileEndEvent(0, @"js_call,init", @{@"json": RCTNullIfNil(json),@"error": RCTNullIfNil(error),});[self handleBuffer:json batchEnded:YES];onComplete(error);}];}];
}

而上述函数则是比较关键的执行函数,需将此函数从RN内核中暴露出来。

模块相互引用

如果要实现按需加载,则主逻辑JS中包含的其他插件JS代码,则不能在主逻辑JS展开的时候运行。我们想要实现这样的效果,则有两个方案可以实施(二选一即可)。

1.跟进JS动态执行的原理,我们可以将主业务JS A中引用插件 B的实现函数使用空方法__d(verboseName + 业务名{空}) 代替。然后等到运行时,再注入相同的方法(__d(verboseName + 业务名{真实方法}) )。等业务触发了插件B逻辑的时候,真正运行的是刚刚注入的B真实方法。

2.懒require

我们平常的业务代码基本是这样引入另外一个模块的

import GameWait from '../gameWait/gameWait';
import NetOperation from '../netOperation/NetOperation';
import GameNight from '../gameOperation/GameNight';
import GameDay from '../gameOperation/GameDay';
import GameState from '../gameState/GameState';
import {GameStateEnum} from '../gameState/GameEnum';

最终打包工具会把他打包成这样的

var _gameWaitGameWait = require('react-native/Werewolf.zip.dir/module/gameWait/gameWait.js');
var _gameWaitGameWait2 = _interopRequireDefault(_gameWaitGameWait);
var _netOperationNetOperation = require('react-native/Werewolf.zip.dir/module/netOperation/NetOperation.js');
var _netOperationNetOperation2 = _ interopRequireDefault(_netOperationNetOperation);
var _gameOperationGameNight = require('react-native/Werewolf.zip.dir/module/gameOperation/GameNight.js');
var _gameOperationGameNight2 = _ interopRequireDefault(_gameOperationGameNight);
var _gameOperationGameDay = require('react-native/Werewolf.zip.dir/module/gameOperation/GameDay.js');
var _gameOperationGameDay2 = _ interopRequireDefault(_gameOperationGameDay);

而这些在业务函数体中,会在编译的时候去找寻此文件是否存在。而这样会报错。

正确的做法是在业务逻辑中,再去require其模块。

if (this.state.nowGameStateEnum === GameStateEnum.game_start) {var GameWait = require('../gameWait/gameWait');this._changeToDay();return (<GameWaitonClosePage={this._onCloseWait.bind(this)}/>);
}

在打包工具中展示则是这样的效果。

if (this.state.nowGameStateEnum === _gameStateGameEnum.GameStateEnum.game_start) {var GameWait = require('react-native/Werewolf.zip.dir/module/gameWait/gameWait.js');this._changeToDay();return (_React2.default.createElement(GameWait, {onClosePage: this._onCloseWait.bind(this)}));
}

这样就实现了require的懒加载。实现了先运行主业务,再动态运行插件业务。

打包工具改造

resolve(ReactPackager.createClientFor(options).then(client => {log('Created ReactPackager');return client.buildBundle(requestOpts).then(outputBundle => {log('Closing client');client.close();return outputBundle;}).then(outputBundle => deleteBaseBundle(outputBundle)).then(outputBundle => processBundle(outputBundle, !args.dev)).then(outputBundle => saveBundleAndMap(outputBundle,args.platform,args['bundle-output'],args['bundle-encoding'],args['sourcemap-output'],args['assets-dest']));}));

打包工具的改造,重要的是将业务Bundle拆分成不同的插件。这个可以仿照以前BaseBundle与业务Bundle拆分的做法。

按需加载小结

RN按需加载,只是一个思路。当业务逐渐庞大的时候,相信大家都会面临这个问题。不过,安卓则比较幸运一点。RN有一个原生的unbundle命令可以将业务Bundle以每个业务一个js文件。不过unbundle命令不能打出iOS平台的,解释是因为iOS上面对小文件有IO性能的瓶颈。不过,这里我就没有亲自测试过了。不过个人感觉,真正做到按需加载,就得根据业务做不同的打包,不易过大,也不易过小。平衡才是王道。


后续

大家从上文耗时表可以了解到,预加载和按需加载,只是优化了启动耗时的一部分。而RN在执行RunApplication到RNComponent展示出,中间还有800ms的耗时。这部分目前来看,不管是狼人杀大型业务的启动,还是demo业务的启动,都会有这800ms的耗时,应该与业务大小无关。从时间表来看,是js在大量绘制ReactNativeBaseComponent。所以,这部分应该也有优化的空间。后续有进展再和大家分享。


更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

React Native按需加载 手Q狼人杀探索之路相关推荐

  1. React Native 按需加载 手Q狼人杀探索之路

    导语:最近特别火的狼人杀和最近特别火的React Native会擦出什么样的火花呢?本文和您一同探讨RN性能优化的现实场景. 项目简介: 狼人杀游戏是多人实时性游戏,对流畅度等性能都有要求.作为大型游 ...

  2. react路由按需加载方法

    使用router4之后以前的按需加载方法require.ensure 是不好使了. 所以我们改用react-loadable插件做按需加载. 第一步: yarn add react-loadable ...

  3. React Native 的图片加载方式

    在做APP的时候,遇到了要加载图片的问题,本来以为很简单,,但是知道真相的我眼泪掉下来.在此记录一下.. 一.本地图片的加载 如上图:./代表当前文件,相当于是在本地项目根据目录找到该图片即可.问题是 ...

  4. React Native 上拉加载

    实现的比较简单,原理就是在下一页有数据的情况下,ScrollView底部始终会有一个loading图表和加载中的文字,每次ScrollView滚动到底部时loading部分就会显示出来,然后去请求数据 ...

  5. react项目如何按需加载antdDesign组件

    react项目如何按需加载antdDesign组件分为两种情况: 第一种手写的,不使用脚手架: 1.安装: npm install ant --save 2.引用: import { Alert, F ...

  6. react按需加载(getComponent优美写法),并指定输出模块名称解决缓存(getComponent与chunkFilename)...

    react配合webpack进行按需加载的方法很简单,Route的component改为getComponent,组件用require.ensure的方式获取,并在webpack中配置chunkFil ...

  7. react 动态路 嵌套动子路由_react 路由动态加载组件,实现按需加载

    默认情况下,当在项目根路径下执行npm run build时,create-react- app内部使用webpack将src/路径下的所有代码打包成一个JS文件和一个 CSS文件. 当项目代码量不多 ...

  8. React单页如何规划路由、设计Store、划分模块、按需加载

    本项目地址:react-coat-helloworld react-coat 同时支持浏览器渲染(SPA)和服务器渲染(SSR),本 Demo 仅演示浏览器渲染,请先了解一下:react-coat 第 ...

  9. 快速搭建react项目骨架(按需加载、redux、axios、项目级目录等等)

    一.前言 最近整理了一下项目骨架,顺便自定义了一个脚手架,方便日后使用.我会从头开始,步骤一步步写明白,如果还有不清楚的可以评论区留言.先大致介绍一下这个骨架,我们采用 create-react-ap ...

最新文章

  1. VS2017离线包下载
  2. 必须为非自相关端口上的非激活接收指定至少一个已初始化的相关集
  3. 为什么选择.NETCore?
  4. adf4351使用_使用ADF绑定创建视图对象行CreateInsert操作
  5. mysql less6教程_Sqli labs系列-less-56 报错注入法(上)
  6. 用java里的junit框架_使用Java JUnit框架里的@SuiteClasses注解管理测试用例
  7. 浮点数在内存中的表示
  8. java 模拟 cmd_用JAVA模拟实现CMD命令行
  9. 不会win10文件夹同步?教你两种方法
  10. 手机型号大全_2000-2500元智能手机最全导购推荐(学生党手机必看)-2020年12月更新...
  11. 【转】数字图像处理课件-艾海舟
  12. python中take函数_Python Pandas Series.take()用法及代码示例
  13. 使用 Kotlin 中的 takeIf
  14. IMDB排名前250名的电影,你看过几部?
  15. led的伏安特性曲线 matlab实现_小灯泡伏安特性曲线实验报告
  16. 素数(质数),合数 ,偶数 , 奇数 ,约数(因数) ,因子 , 质因子 , 哥德巴赫猜想定义
  17. 微信可以改彩色昵称了!!!太sao了吧!
  18. C++第8周项目3小贺的工资
  19. 股票波段+趋势+主力动向 三合一指标公式(无未来函数)
  20. 世界杯,越位,点球,角球等足球相关英语怎么说

热门文章

  1. 小白入坑【文献阅读】之前需要知道的软件、网站、常识
  2. 如何成为一名职业黑客?
  3. 如何使用思科自带抓包工具
  4. 天猫店群玩法,天猫无货源店群这样操作,优化宝贝不降权!
  5. 华为视频会议,助力企业往高品质发展
  6. springboot毕设项目高校课程知识库系统hd2m4(java+VUE+Mybatis+Maven+Mysql)
  7. CentOS7 安装98五笔输入法
  8. 在python内置集成开发环境中可使用快捷键什么_2020知道智慧树普通话训练与测试题库及答案...
  9. 关于人生,理想和抉择的对话
  10. 如何写一封好的简历?