以下文章是《React Native 优化终极指南》的一部分,介绍了如何在原生和 JavaScript 之间找到平衡。
为什么这很重要?
React Native 可以让您创建快速运行、易于维护的应用程序。但是,要实现这一点,您首先需要在本机和 JavaScript 之间找到平衡。否则,您的应用程序可能会明显变慢,因为简单地说,它会超载。由于对应用程序性能的任何损害都等同于对业务的损害,因此我们决定与您分享一些找到这种和谐的最佳实践。
在以《React Native 优化终极指南》为基础的其他博文中,我们谈到了以下与通过了解 React Native 实现细节来提高性能有关的主题:
- 为您的应用程序选择合适的外部库
- 使用移动专用库优化电池消耗
- 以 60FPS 制作动画
- 使用高阶组件提高 React Native 性能
- 优化 React Native 应用程序的 JavaScript 捆绑程序
请务必查看这些内容。现在,让我们进入正题。
在原生代码和 JavaScript 之间找到平衡
在使用 React Native 时,您大部分时间都要开发 JavaScript。不过,在某些情况下,你也需要编写一些原生代码。例如,您使用的第三方 SDK 还没有正式的 React Native 支持。在这种情况下,你需要创建一个原生模块来封装底层的原生方法,并将它们导出到 React Native 领域。
所有原生方法都需要真实世界的参数才能工作。React Native 构建在一个名为桥(bridge)的抽象之上,它在 JavaScript 和原生世界之间提供双向通信。因此,JavaScript 可以执行原生 API,并传递必要的上下文以接收所需的返回值。这种通信本身是异步的,这意味着当调用者在等待本地端返回结果时,JavaScript 仍在运行,并可能已经开始执行另一项任务。
到达桥接器的 JavaScript 调用次数并不是确定的,可能会随着时间的推移而变化,这取决于您在应用程序中进行的交互次数。此外,每次调用都需要时间,因为 JavaScript 参数需要被串化为 JSON,而 JSON 是这两个领域都能理解的既定格式。
例如,当您的网桥忙于处理数据时,另一个调用将不得不阻塞和等待。如果该交互与手势和动画有关,则很可能会出现丢帧现象--某些操作没有执行,导致 UI 出现抖动。
某些库(如 Animated)提供了特殊的解决方法。在本例中,我使用了 NativeDriver,它将动画序列化,并将其传递给原生线程,而且在动画运行时不会通过桥接器,从而防止在其他工作正在进行时意外掉帧。
这就是保持桥接通信高效快速的原因。
更多的车流通过桥梁意味着更少的空间用于其他事情
在桥接器上传输更多流量意味着 React Native 可能会减少空间来传输其他重要内容。因此,在执行本机调用时,应用程序可能会对手势或其他交互反应迟钝。
如果您在桥接器上执行某些本机调用时看到 UI 性能下降,或者看到大量的 CPU 消耗,那么您应该仔细检查一下外部库的工作情况。很有可能是传输的内容超出了应有的范围。
在 JS 端使用适量的抽象 - 提前验证和检查类型
在构建本机模块时,很容易将调用立即代理到本机端,然后让本机来完成剩下的工作。然而,在某些情况下,比如参数无效,最终会导致不必要的桥上往返,却发现我们没有提供正确的参数集。
让我们使用一个简单的 JavaScript 模块,它只需将调用直接代理到底层的本地模块即可。
import { NativeModules } from "react-native";
const { ToastExample } = NativeModules;
export const show = (message, duration) => {
ToastExample.show(message, duration);
};
绕过本地模块参数
如果参数不正确或丢失,原生模块可能会抛出异常。当前版本的 React Native 没有提供抽象概念来确保 JavaScript 参数与原生代码所需的参数保持同步。您的调用将被序列化为 JSON,传输到原生端并执行。
尽管我们没有传入完整的参数列表,但该操作会顺利执行。当本地端处理调用并收到来自本地模块的异常时,错误就会在下一个 tick 中出现。
在这样一个简单的场景中,我们在等待异常时损失了一些时间,而我们本可以事先检查异常的。
import { NativeModules } from "react-native";
const { ToastExample } = NativeModules;
export const show = (message, duration) => {
if (typeof message !== "string" || message.length > 100) {
throw new Error("Invalid Toast content");
}
if (!Number.isInteger(duration) || duration > 20000) {
throw new Error("Invalid Toast duration");
}
ToastExample.show(message, duration);
};
使用带参数验证的原生模块
上述内容不仅与原生模块本身有关。请记住,每个 React Native 原始组件都有其原生等价物,每次发生渲染时,组件道具都会在桥上传递;就像你使用 JavaScript 参数执行原生方法一样。
为了更好地理解这一点,让我们仔细看看 React Native 应用程序中的样式设计。
import React from "react";
import { View } from "react-native";
const App = () => {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
backgroundColor: "coral",
width: 200,
height: 200,
}}
/>
</View>
);
};
export default App;
样式化组件的最简单方法是向其传递一个带有样式的对象。虽然这种方法可行,但您不会经常看到。除非您要处理的是动态值,例如根据状态改变组件的样式,否则一般都会认为这是一种反模式。
import React from "react";
import { View, StyleSheet } from "react-native";
const App = () => {
return (
<View style={styles.container}>
<View style={styles.box} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
box: {
backgroundColor: "coral",
width: 200,
height: 200,
},
});
export default App;
大多数时候,React Native 会使用 StyleSheet API 在桥上传递样式。该 API 会处理你的样式,并确保它们只在桥上传递一次。在运行时,它会将样式道具的值替换为与本地缓存样式相对应的唯一数字标识符。
因此,React Native 在每次重新渲染 UI 时都会发送一个大型对象数组,而现在桥接器只需处理一个数字数组,这样就更易于处理和传输。
原生与 JavaScript 之间的平衡使代码库更快、更易维护
无论您现在是否面临任何性能挑战,围绕原生模块实施一套最佳实践都是明智之举,因为其好处不仅在于速度,还在于用户体验。
当然,在桥上保持适量的流量最终将有助于应用程序更好地运行和流畅地工作。正如您所看到的,本文中提到的某些技术已经在 React Native 中得到了积极的应用,为您提供了令人满意的开箱即用性能。了解这些技术将有助于您创建在重负载下性能更佳的应用程序。
值得指出的另一个好处是维护。
将验证等繁重的高级抽象保留在 JavaScript 端,将导致原生层变得非常薄,而原生层只不过是底层原生 SDK 的一个封装而已。换句话说,模块的本地部分看起来更像是从文档中直接复制粘贴--可理解且具体。
掌握了这种开发本地模块的方法,很多 JavaScript 开发人员就可以轻松地为应用程序扩展额外的功能,而无需专门学习 Objective-C 或 Java。
需要性能优化方面的帮助?
我们是 React Native 的 Facebook 官方合作伙伴。我们在 React Native 项目上已经工作了 5 年多,为客户提供了高质量的解决方案,并为 React Native 生态系统做出了巨大贡献。我们的开源项目每天都在帮助成千上万的开发人员应对挑战,让他们的工作变得更轻松。如果您需要跨平台或 React Native 开发方面的帮助,请联系我们。