市场中的 React Native 性能
React Native 在 Facebook 家族的多个应用中都有使用,包括主 Facebook 应用中的顶层标签页。本文将重点介绍一个高度可见的产品——Marketplace。它已经在十几个国家/地区上线,让用户可以发现其他用户提供的商品和服务。
在 2017 年上半年,通过 Relay 团队、Marketplace 团队、Mobile JS Platform 团队和 React Native 团队的共同努力,我们将 Marketplace 在 Android 2010-11 年设备上的交互时间 (TTI) 缩短了一半。Facebook 过去一直将这些设备视为低端 Android 设备,它们的 TTI 在任何平台或设备类型中都是最慢的。
典型的 React Native 启动过程如下所示:

免责声明:比例不具代表性,会因 React Native 的配置和使用方式而异。
我们首先初始化 React Native 核心(即“Bridge”),然后运行特定于产品的 JavaScript,它决定了 React Native 将在原生处理时间中渲染哪些原生视图。
一种不同的方法
我们早期犯的一个错误是让 Systrace 和 CTScan 来驱动我们的性能优化工作。这些工具在 2016 年帮助我们发现了许多容易解决的问题,但我们发现 Systrace 和 CTScan **都不能代表生产场景**,也无法模拟实际情况。 breakdowns 中时间分配的比例常常是不正确的,有时甚至完全错误。极端情况下,一些我们认为只需要几毫秒的事情实际上需要数百甚至数千毫秒。尽管如此,CTScan 仍然很有用,我们发现它可以在故障进入生产环境之前捕获三分之一的回归问题。
在 Android 上,我们将这些工具的缺点归因于以下事实:1) React Native 是一个多线程框架,2) Marketplace 与 Newsfeed 和其他顶级选项卡等众多复杂视图共存,3) 计算时间差异巨大。因此,本半年,我们几乎所有的决策和优先级都由生产测量和细分来驱动。
深入生产环境检测
表面上,生产环境检测可能听起来很简单,但事实证明这是一个相当复杂的过程。它经历了多个迭代周期,每个周期持续 2-3 周;由于将提交合并到主分支、将应用程序推送到 Play 商店以及收集足够的生产样本以确保我们工作的准确性存在延迟。每个迭代周期都涉及发现我们的细分是否准确,它们是否具有正确的粒度,以及它们是否正确地加总到整个时间跨度。我们不能依赖 alpha 和 beta 版本,因为它们不代表一般用户群体。本质上,我们非常细致地构建了一个基于数百万样本聚合的非常准确的生产跟踪。
我们之所以一丝不苟地验证细分中的每一毫秒都正确地加总到它们的父指标,原因之一是我们很早就意识到我们的检测存在空白。事实证明,我们最初的细分并未考虑到线程跳转导致的停顿。线程跳转本身并不昂贵,但跳转到已经在忙碌的线程却非常昂贵。我们最终通过在正确的时间点添加 Thread.sleep() 调用来在本地重现这些阻塞,并通过以下方式成功修复了它们:
- 移除对 AsyncTask 的依赖,
- 撤销在 UI 线程上强制初始化 ReactContext 和 NativeModules,以及
- 消除初始化时测量 ReactRootView 的依赖。
综合来看,消除这些线程阻塞问题将启动时间缩短了25%以上。
生产环境指标也挑战了我们之前的一些假设。例如,我们过去会预先加载许多 JavaScript 模块到启动路径中,认为将模块放在同一个 bundle 中可以降低它们的初始化成本。然而,预加载和共用这些模块的成本远远超过了其带来的好处。通过重新配置我们的内联 require 黑名单,并从启动路径中移除 JavaScript 模块,我们得以避免加载不必要的模块,例如 Relay Classic(当时只需要 Relay Modern)。今天,我们的 RUN_JS_BUNDLE breakdown 速度提高了 75% 以上。
我们还通过调查特定产品原生模块发现了胜利。例如,通过惰性注入原生模块的依赖项,我们将该原生模块的成本降低了 98%。通过消除 Marketplace 启动与其他产品之间的竞争,我们相应地缩短了启动时间。
最棒的是,其中许多改进都广泛适用于所有使用 React Native 构建的屏幕。
结论
人们通常认为 React Native 启动性能问题是由于 JavaScript 运行缓慢或网络时间过长造成的。虽然加快 JavaScript 等会使 TTI 降低一个不小的总和,但这些因素对 TTI 的贡献百分比远低于之前的估计。
到目前为止,我们学到的经验是:*测量、测量、再测量!* 一些优化是通过将运行时成本转移到构建时来实现的,例如 Relay Modern 和 Lazy NativeModules。另一些优化是通过更智能地并行化代码或移除死代码来避免不必要的工作。还有一些优化来自于 React Native 的重大架构变更,例如清理线程阻塞。性能没有万能的解决方案,长期的性能提升将来自于渐进式的检测和改进。不要让认知偏差影响你的决策。相反,仔细收集和解读生产数据来指导未来的工作。
未来计划
从长远来看,我们希望 Marketplace 的 TTI 能够与原生构建的类似产品相媲美,并且总的来说,React Native 的性能能够与原生性能持平。此外,虽然本季度我们已将 bridge 的启动成本大幅降低了约 80%,但我们计划通过 Prepack 等项目以及更多的构建时处理,将 React Native bridge 的成本降至接近零。