携程React Native实践
React Native(下文简称 RN)开源已经一年多时间,国内各大互联网公司都在使用,携程也在今年 5 月份投入资源开始引入,并推广给多个业务团队使用,本文将会分享我们遇到的一些问题以及我们的优化方案。 一、背景和使用情况介绍 为什么会引入 React Native?1. AppSize 占用
2. 用户体验佳
3. 相对成熟
4. 支持动态更新
5. 跨平台
如何引入? 基于 RN 0.30 版本,开发了支持携程业务团队快速便捷开发的 CRN 框架,框架主要从以下几个方面着手。 1. 工具
2. 控件
3. 稳定性、性能优化
4. 发布
除此之外,我们还从文档以及技术支持等方面,支撑其作为一个完整的产品开发框架。 业务的使用 下面一幅图说明了 RN 在携程业务中的使用情况,总共 4 个版本的开发时间,每个版本大约 1 个月时间。 前面 2 个版本主要是 CRN 基础功能完成和线上验证,后面 2 个版本稳定性优化和 API 跨平台抹平基本完成,业务数和页面数量猛增。 二、遇到的问题和优化 RN 常见问题介绍 所有做 React Native 开发的团队,或多或少都面临着以下 4 个问题需要解决。
接下来,我们就这四个问题来一一探讨。 从这张图中可以看出,最大的瓶颈在 JS init + Require,这块时间就是 JSBundle 的执行时间,为了提升页面加载速度,这块时间我们需要想办法优化。 JSBundle 文件过大 & 页面加载慢 先来说一组数据,一个 Helloorld 的 App,如果使用 0.30 RN 官方命令 如果只有一两个业务使用,这点大小算不了什么,但是对于我们这种动辄几十个业务的场景,如果每个业务的 JSBundle 都需要这么大的一个 RN 框架本身,那将是不可接受的。 因此,我们需要对 RN 官方的打包脚本做改造,将框架代码拆分出来,让所有业务使用一份框架代码。 开始拆分之前,我们先以 Hello World 的 RN App 为基础介绍几个背景知识。 上述是一个 Hello World RN App 代码的结构,基本分为 3 部分:
上述是 Hello World RN App 打包之后 JSBundle 文件的结构,基本分为 3 部分:
如果所有业务代码,都遵照一个规则:入口 JS 文件首先 require 的都是 react/react-native,则打包生成的 JSBundle 里面 react/react-native 相关的模块 拆分方案一基于上面 2 点背景知识介绍,我们很容易发现,如果将打包之后的 JSBundle 文件,拆分成 2 部分(框架部分+业务模块部分),使用的时候合并起来,然后去加载,即可实现拆分功能。 具体实现步骤:
貌似功能完成,可是回到 Dive into React Native performance,这么做还是优化不了 JSBundle 的执行时间。因为我们不能把拆分开的 2 个文件分别执行,加载 显然,这种拆分方式不能满足我们这种需要。 那这个方案就完全没有价值吗?不是的,如果你做的是一个纯 RN App,Native 只是一个壳,里面业务全是 RN 开发的,完全可以使用这种方式做拆分,这种方案简单,无侵入,实现成本低,不需要修改任何 RN 打包代码和 RN Runtime 代码。 拆分方案二 RN 框架部分文件(common.js)大小 530KB,如此大的 JS 文件,占用了绝大部分的 JS 执行时间。这块时间如果能放到后台预先做完,进入业务也只需执行业务页面的几个 JS 文件,将可以大大提升页面加载速度,参考上面的 RN 性能瓶颈图,预估可以提升 100%。 按照这个思路,能后台加载的 JS 文件,实际上是就是一个 RN App。因此我们设计了一个空白页面的 Fake App,它做一件事情,就是监听要显示的真实业务 JS 模块,收到监听之后,渲染业务模块,显示页面。 Fake App 设计如下: 为了实现该拆包方案,需要改造 React-Native 的打包命令;
改造页面加载流程:
通过后台预加载,省去了绝大部分的 JS 加载时间,似乎问题已经完美解决。 但是,如果随着业务不断膨胀,一个 RN 业务 JS 代码也达到 500KB,进入这个业务页面,500 多KB 的 JS文件读取出来,执行,整个 JS 执行的时间瓶颈会再次出现。 拆分方案三 正在此时,我们研究 RN 在 Facebook App 里面的使用情况,发现了 下面截图就是
RN 里面加载模块流程说明,以 require(66666) 模块为例:
打包通过 顺便提一下,这个 Unbundle 方案,只在 Android 上有效,打 iOS 平台的 Unbundle 包,是打不出来的。在 RN 的打包脚本上有一行注释,大致意思是在 iOS 上众多小文件读取,文件 IO 效率不够高,Android 上没这样的问题,然后判断如果是打 iOS 的 Unbundle 包的时候,直接 return 了。 相对应的,iOS 开发了一个 prepack 的打包模式,简单点说,就是把所有的 JS 模块打包到一个文件里面,打包成一个二进制文件,并固定 0xFB0BD1E5 为文件开始,这个二进制文件里面有个 meta-table,记录各个模块在文件中的相对位置,在加载模块 (require)的时候,通过 fseek,找到相应的文件开始,读取,执行。 在 Unbundle 的启发下,我们修改打包工具,开发了 CRNUnbunle,做了简单的优化,把众多零散的 JS 文件做了简单的合并。 将 common 部分的 JS 文件,合并成一个
做完这个拆包和加载优化之后,我们用自己的几个业务做了下测试,下图是当时的测试验证数据。 可以看出,iOS 和 Android 基本都比官方打包方式的加载时间,减少了 50%。 这是自己单机测试的数据,那上线之后,数据如何呢? 下图,是我们分析一天的数据,得出的平均值&;排除掉了 5s 以上的异常数据,后面实测下来 5s 以上数据极少>; 看到这个数据,发现和我们自己测试的基本一致,但是还有一个疑问,加载的时间分布,是否服从正态分布,会不会很离散,快的设备很快,慢的设备很慢呢? 然后我又进一步分析这一天的数据,按照页面加载时间区间分布统计。 看图上数据,很明显,iOS & Android 基本一致,将近 98% 的用户都能在 1s 内加载完成页面,符合我们期望的正态分布,所以 bundle 拆分到此基本完成。 关于这个数据,补充一下,先前已看到一篇58同城同学分享的RN实践的文章,里面也曾提到他们业务页面加载时间的数据,有兴趣的同学可以去比较下。 页面加载优化 按照上述的拆包方案实现后,我们的 RN 页面加载流程大致是这样的。 从上文的优化可以看出,缓存了 缓存,还是缓存,不要立即释放,等符合一定条件之后,再释放。 对JS执行引擎,我们定义了以下的一些生命周期状态。
错误处理 RN 刚上线的前 2 个版本,我们发现有大量因为 RN 导致的 Crash,常见的错误有以下几种。 iOS 的 Crash 问题处理 iOS 的 Crash,基本都来自 void RCTSetFatalHandler(RCTFatalHandler fatalHandler); 一般初次开发 RN 应用的开发人员,都没有留意这一点,其实查阅下 RN 的源代码, Android 的 Crash 问题处理 Android 的 Crash 点相对较多,大致会出现在以下几个场景:
对于第一点提到的 不能连接到 偶现的 JavaScript 执行出错,怎么会走到 问题的解决很简单,这些 再补充一点,这些错误处理之后,都需要一层一层的传递到最上层的 UI 界面,这样才能友好地给用户提示。 ListView 性能问题 先来看一张截图,是从 RN 提供的 UIExplore Demo 跑出来的: 可以清楚的看到,超出屏幕的条目,依然被渲染了。没有实现 cell 重用,导致数据量大时候,卡顿。 为适应大数据量 ListView 的场景,我们专门安排资源,开发了可重用 cell 的 实际测试下来,数据量少时候,和 RN 提供的 ListView,性能基本一致,但当数据量大时候, 三、下一阶段的规划 1. CRN-Web 的开发 同样的功能,CRN 一套代码可以在 iOS 和 Android 2 个平台运行。但对于业务开发团队,他们还需要维护 H5 平台同样的功能。如果我们能够将 CRN 代码,通过类似 webpack 这样的工具,直接转换过去就能在 H5 平台上运行起来,就可以做到一套代码,三端运行,可以大大降低业务团队的开发维护成本。 目前,我们已经再拿一些业务的 CRN 代码做转换验证,初步验证可行,还在持续优化完善中。 2. 单JS执行引擎的实现 RN 还有一个比较大的性能瓶颈在于内存耗用大。做过这样的测试,在一个 Hello World 的 RN 工程里面,打开一个 Native/RN/H5 Hybrid 的 Hello World 页面,Native 显示页面内存占用 0.2MB,RN 占用 10MB,H5 Hybrid 占用 20MB。如果大量业务都使用 RN 开发,JS 执行引擎大量创建,会耗费大量内存,但是从 JS 执行引擎的执行过程。运行逻辑来说,只要做好业务隔离,完全是可以在一个执行引擎里面运行多个业务功能的 JS 代码的。我们正在做相关尝试,相信在未来 1-2 个版本时间,可以完成线上验证。 3. AMD模式的加载尝试 RN 打包默认是CommonJS规范,整个 JSBundle 一次读入内存,一次全部执行完成,所以耗费大量时间。如果能够用 AMD 模式改造,JSBundle 读取到内存,但是只执行用到的模块,真正做到按需加载,相信对页面加载效率,会有更近一步的提升。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |