React Native 在 Marketplace 中的性能
React Native 在 Facebook 系列的多个应用中的多个位置被使用,包括主要 Facebook 应用中的顶级标签。本文的重点是一个高度可见的产品,Marketplace。它在十几个国家/地区可用,使用户能够发现其他用户提供的产品和服务。
在 2017 年上半年,通过 Relay 团队、Marketplace 团队、Mobile JS Platform 团队和 React Native 团队的共同努力,我们将 Android Year Class 2010-11 设备的 Marketplace 交互时间 (TTI) 缩短了一半。Facebook 历来将这些设备视为低端 Android 设备,并且它们在任何平台或设备类型上都具有最慢的 TTI。
典型的 React Native 启动看起来像这样
免责声明:比例不具代表性,并且会因 React Native 的配置和使用方式而异。
我们首先初始化 React Native 核心(又名“Bridge”),然后再运行特定于产品的 JavaScript,该 JavaScript 确定 React Native 将在原生处理时间中渲染哪些原生视图。
一种不同的方法
我们早期犯下的错误之一是让 Systrace 和 CTScan 驱动我们的性能工作。这些工具帮助我们在 2016 年找到了许多唾手可得的成果,但我们发现 Systrace 和 CTScan 都不能代表生产场景,也无法模拟野外发生的情况。细分的时间比率通常是不正确的,有时会完全失真。在极端情况下,我们期望花费几毫秒的事情实际上花费了数百或数千毫秒。尽管如此,CTScan 仍然很有用,我们发现它在回归问题影响生产之前捕获了三分之一的回归问题。
在 Android 上,我们将这些工具的缺点归因于以下事实:1) React Native 是一个多线程框架,2) Marketplace 与大量复杂视图(例如 Newsfeed 和其他顶级标签)共存,以及 3) 计算时间变化很大。因此,这半年,我们让生产测量和细分驱动了我们几乎所有的决策和优先级排序。
沿着生产工具化的道路
表面上看,工具化生产可能听起来很简单,但事实证明这是一个相当复杂的过程。它花费了多个 2-3 周的迭代周期;这是因为将提交落地到主分支、将应用推送到 Play 商店以及收集足够的生产样本以对我们的工作充满信心都存在延迟。每个迭代周期都涉及到发现我们的细分是否准确,它们是否具有正确的粒度级别,以及它们是否正确地加起来等于整个时间跨度。我们不能依赖 alpha 和 beta 版本,因为它们不能代表普通人群。本质上,我们非常乏味地构建了一个非常准确的生产跟踪,该跟踪基于数百万个样本的聚合。
我们一丝不苟地验证细分中的每一毫秒都正确地加起来等于其父指标的原因之一是,我们很早就意识到我们的工具化存在差距。事实证明,我们最初的细分没有考虑到线程跳转造成的停顿。线程跳转本身并不昂贵,但是跳转到已经很忙的线程(正在工作)非常昂贵。我们最终通过在正确的时刻散布 Thread.sleep()
调用来在本地重现了这些阻塞,并且我们通过以下方式设法修复了它们:
- 移除我们对 AsyncTask 的依赖,
- 撤消在 UI 线程上强制初始化 ReactContext 和 NativeModules,以及
- 移除在初始化时测量 ReactRootView 的依赖。
总之,移除这些线程阻塞问题将启动时间缩短了 25% 以上。
生产指标也挑战了我们之前的一些假设。例如,我们过去常常在启动路径上预加载许多 JavaScript 模块,假设将模块共置在一个捆绑包中会降低它们的初始化成本。然而,预加载和共置这些模块的成本远远超过了收益。通过重新配置我们的内联 require 黑名单并从启动路径中删除 JavaScript 模块,我们能够避免加载不必要的模块,例如 Relay Classic(当只需要 Relay Modern 时)。如今,我们的 RUN_JS_BUNDLE
细分速度提高了 75% 以上。
我们还通过调查特定于产品的原生模块找到了优势。例如,通过延迟注入原生模块的依赖项,我们将该原生模块的成本降低了 98%。通过消除 Marketplace 启动与其他产品的竞争,我们等效地缩短了启动时间。
最好的部分是,这些改进中的许多都广泛适用于使用 React Native 构建的所有屏幕。
结论
人们认为 React Native 启动性能问题是由 JavaScript 速度慢或网络时间过高引起的。虽然加速 JavaScript 之类的事情会使 TTI 降低相当大的幅度,但这些因素各自对 TTI 的贡献百分比远低于之前认为的。
到目前为止的教训是测量,测量,再测量! 一些胜利来自将运行时成本转移到构建时间,例如 Relay Modern 和 Lazy NativeModules。其他胜利来自通过更智能地并行化代码或删除死代码来避免工作。还有一些胜利来自 React Native 的大型架构更改,例如清理线程阻塞。性能没有万能的解决方案,更长期的性能提升将来自增量工具化和改进。不要让认知偏差影响你的决策。相反,仔细收集和解释生产数据以指导未来的工作。
未来计划
从长远来看,我们希望 Marketplace TTI 与使用 Native 构建的类似产品相当,并且总的来说,React Native 性能与原生性能相当。此外,尽管这半年我们大幅降低了大约 80% 的桥启动成本,但我们计划通过 Prepack 和更多构建时处理等项目,将 React Native 桥的成本降至接近于零。