性能调优是任何应用程序开发工作的重要组成部分。在本文中,我将讨论我们用于识别和解决基于 ReactXP 的 Skype 应用程序中性能瓶颈的一些工具和技术。
跨平台代码库的好处之一是,许多性能改进对所有平台都有益。
测量和分析
有人说,你无法改进你无法衡量的事物。这对于性能调优尤其如此。我们使用各种工具来确定哪些代码路径对性能至关重要。
无论使用哪种分析工具,在测量性能时,您可能都希望使用应用程序的生产版本。React JavaScript 代码在“开发模式”下执行时会执行许多耗时的运行时检查,这可能会显著扭曲您的测量结果。
Chrome 性能工具
Chrome 浏览器提供了出色的跟踪和可视化工具。打开开发者工具窗口,单击“性能”选项卡,然后单击“记录”按钮。记录完成后,Chrome 将显示一个包含调用层次结构的详细时间轴。放大和缩小以确定时间都花在了哪里。
Systrace
React Native 提供了一种启用和禁用 Systrace 的方法,Systrace 是一种方法级跟踪记录工具。它记录本机和 JavaScript 方法,因此可以很好地概述整个应用程序中发生的情况。要使用 Systrace,请构建开发版本并将其部署到您的设备。摇动设备以显示开发者菜单(如果您在 iOS 模拟器中运行,请按 command-D)。选择“Start Systrace”,然后执行您要测量的操作。当您停止 Systrace 时,将创建一个 HTML 跟踪文件。您可以在 Chrome 中可视化并与此跟踪进行交互。最新版本的 Chrome 弃用了 Systrace 代码中使用的功能,因此您需要按如下方式编辑它。只需将以下行添加到生成的 HTML 文件的头部部分。
<script src="https://rawgit.com/MaxArt2501/object-observe/master/dist/object-observe.min.js"></script>
控制台日志记录
原始控制台日志记录通常是衡量性能的有效方法。日志条目可以以毫秒分辨率的时间戳发出。只需调用 Date.now() 即可获取当前时间。性能关键操作(如应用程序启动)的持续时间也可以在日志中计算和输出。
插桩
一旦您的应用程序大规模部署,监控关键操作的性能就非常重要。为此,我们会记录发送到我们服务器并聚合所有用户数据的插桩。然后,我们可以按时间、按平台、按设备类型等可视化数据。
跨越桥接
React Native 应用程序包含两个独立的执行环境——JavaScript 和 Native。这些环境相对独立。它们各自在单独的线程上运行,并可以访问自己的数据。两个环境之间的所有通信都通过 React Native“桥接”进行。您可以将桥接视为一个双向消息队列。消息按照它们在每个队列中放置的顺序进行处理。
数据以序列化形式传递——JSON 格式的 UTF16 文本。所有 I/O 都发生在原生环境中。这意味着 JavaScript 代码发起的任何存储或网络请求都必须通过桥接,然后结果数据必须序列化并沿另一个方向通过桥接发送回去。这对于少量数据来说效果很好,但是一旦数据大小或消息数量增加,它就会变得昂贵。
缓解此瓶颈的一种方法是避免通过桥接传递大量数据。如果它不需要在 JavaScript 环境中处理,请将其保留在原生端。它可以在 JavaScript 代码中表示为“句柄”。这就是我们处理所有图像、声音和复杂动画定义的方式。
协作式多任务处理
JavaScript 在单个线程上运行。如果您的应用程序的 JavaScript 代码长时间运行,它会阻塞事件处理程序、消息处理程序等的执行,并且应用程序会感觉无响应。如果您需要执行长时间运行的操作,您有几个选择
- 将其实现为原生模块并在单独的线程上运行(仅适用于 React Native)。
- 将操作分解成更小的块,并将它们作为链式任务执行。
- 仅计算当时所需结果的一部分。
虚拟化
在处理用户界面中出现的大量数据列表时,使用某种形式的虚拟化非常重要。虚拟化视图仅渲染可见内容。当用户滚动列表时,新显示的项目会被渲染。我们查看了所有可用的虚拟化视图,但我们没有找到任何能够提供我们所需的速度和灵活性的视图,因此我们最终编写了自己的实现。我们的 VirtualListView 经过了六次主要迭代,才最终确定了一个我们满意的设计和实现。
启动您的应用程序
应用程序启动时间可能是 React Native 应用程序面临的最大性能挑战。在较慢的 Android 设备上尤其如此。我们仍在努力减少此类设备上的启动时间。以下是我们在此过程中学到的一些技巧。
推迟模块初始化
在 TypeScript 或 JavaScript 代码中,在每个模块的顶部包含一堆 import 语句是很常见的做法。例如,这是您在 hello-world 示例的 App.tsx 文件顶部找到的内容。
import RX = require('reactxp');
import MainPanel = require('./MainPanel');
import SecondPanel = require('./SecondPanel');
这些“require”调用中的每一个都会在第一次遇到时初始化指定的模块。然后缓存对该模块的引用,因此后续调用“require”相同模块几乎是免费的。在启动时,第一个模块需要几个其他模块,每个模块又需要几个其他模块,依此类推。这会一直持续到整个模块依赖树都初始化完毕。这一切都发生在您的第一个模块的第一行执行之前。随着应用程序中模块数量的增加,初始化时间也会增加。
解决此问题的方法是通过延迟初始化。为什么要在启动时为某个不常用的 UI 面板的模块初始化付出代价呢?只需将其初始化推迟到需要时。为此,我们使用了 Facebook 创建的一个 babel 插件,名为 inline-requires。只需下载脚本并创建一个看起来像这样的 “.babelrc” 文件
{
"presets": ["react-native"],
"plugins": ["./build/inline-requires.js"]
}
这个脚本是做什么的?它消除了模块顶部的 require 调用。每当导入的变量在文件中使用时,它都会插入一个 require 调用。这意味着所有模块都会在其首次使用之前立即初始化,而不是在应用程序启动时初始化。对于大型应用程序,这可以在较慢的设备上将应用程序的启动时间缩短数秒。
代码压缩
对于生产版本,对 JavaScript 进行“压缩”非常重要。此过程会消除多余的空格,并尽可能缩短变量和方法名称。它会减少 JavaScript 包在磁盘和内存中的大小,并加快代码的解析速度。
原生模块初始化
React Native 包含许多内置的“原生模块”。这些模块提供您可以从 JavaScript 调用的功能。许多应用程序不会使用所有默认的原生模块。每个原生模块都会为应用程序初始化时间增加数十毫秒,因此初始化应用程序不会使用的原生模块是浪费的。在 Android 上,您可以通过创建特定于您的应用程序的 MainReactPackage 子类来消除这种开销。将 createViewManagers() 方法复制到您的子类中,并注释掉您不使用的视图管理器。然后更改应用程序 ReactInstanceHost 类中的 getPackages() 方法,以实例化您的自定义类而不是正常的 MainReactPackage。此技术可以在慢速 Android 设备上将启动时间缩短 100 毫秒或更多。
额外资源
有关性能调优的其他提示,请参阅 Facebook 的 React Native 文档网站上的性能页面。此博客也包含有用的提示。