让我们开始了解使用 JavaScript 通用进行丰富平台开发的性能指标。
NativeScript TSC 与“React Native Avengers”、Margelo 联手,深入研究利用 JavaScript 进行丰富开发实践的各个方面。我们共同致力于不断发展、优化和丰富广阔的 JavaScript 生态系统。
未来探索的各种基准测试
JavaScript 已成为一种普遍流行的编程语言,因此它自然而然地被用于各种不同的平台开发,例如后端服务器基础设施以及桌面和移动客户端应用程序。
所有应用程序都以某种形式或方式涉及访问平台 API(例如绘制 UI、跟踪手势、访问摄像头等等)。大多数平台 API(Web 平台之外)通常不是用 JavaScript 编写的(例如,大多数 iOS API 都是用 Objective-C 和 Swift 编写的)。因此,为了继续在您选择的平台目标上使用 JavaScript,在语言之间交互时有一些指标需要了解。
这在实践中究竟有多大成本,需要进行深入透彻的分析才能正确理解。缺乏清晰且可重复的测试用例以及相应的基准测试导致犹豫使用 JavaScript 为原生平台构建应用程序。因此,我们 NativeScript 技术指导委员会有兴趣量化在多个性能指标上使用 JavaScript 进行原生应用程序开发的开销,以满足理解的需要。
一些预先考虑因素
由于基准测试对于决策者理解的根本性,我们高度重视基准测试的共享,因此我们认为有必要重申
共享性能基准测试 101:
为了扩展我们的视野,我们决定在每个基准测试中对 NativeScript 和 React Native 应用程序以及纯原生 Objective C 应用程序进行分析,针对精确的测试用例,在第一部分中只关注 iOS。
我们使用了这两个库的最新版本,使用了推荐的 JavaScript 运行时和调用原生 API 的最佳方法
名称 | 库 | 运行时 | 方法 |
---|---|---|---|
NativeScript | @nativescript/[email protected] |
V8 | 核心 |
React Native | [email protected] |
Hermes | 基于 JSI 的 原生模块 |
iOS Objective C 应用程序使用 Xcode 13.4.1 进行基准测试。
性能指标代表了每种方法可以通过其 JavaScript 引擎与目标平台进行通信的“编组”速度的测试。“编组”一词用在您跨越某种边界时。在不同设备上的 JavaScript 情况下,边界是特定于语言的,这意味着 JavaScript 与目标平台的自然平台语言进行通信(例如,iOS 上的 Swift 或 Objective C)。
测试用例使用一个名为 TestFixtures
的单个 Objective C 类,其中包含 3 种不同的方法,如下所示
TestFixtures.m
:#import "TestFixtures.h"
@implementation TestFixtures
- (int32_t)methodWithX:(int32_t)x Y:(int32_t)y Z:(int32_t)z {
return x + y + z;
}
- (NSString*)methodWithString:(NSString*)aString {
return aString;
}
- (UIImage *)methodWithBigData:(NSArray *)array {
uint8_t *bytes = malloc(sizeof(*bytes) * array.count);
size_t i = 0;
for (id value in array) {
bytes[i++] = [value intValue];
}
NSData *imageData = [NSData dataWithBytesNoCopy:bytes length:array.count freeWhenDone:YES];
UIImage *image = [UIImage imageWithData:imageData];
return image;
}
@end
(int32_t)methodWithX:(int32_t)x Y:(int32_t)y Z:(int32_t)z
:练习原始数据类型,例如数字
(NSString*)methodWithString:(NSString*)aString
:练习字符串
(UIImage *)methodWithBigData:(NSArray *)array
:练习大型数据处理,例如二进制图像
每个基准测试都将使用通过 JavaScript 循环执行的相同测量,以确定哪种方法可以从 JavaScript 最快地与自然主机平台 API 进行通信。循环将按如下方式实现
测量函数
function measure(name: string, action: () => void) {
const start = performance.now();
action();
const stop = performance.now();
console.log(`${stop - start} ms (${name})`);
}
测量目标
measure("Primitives", function () {
for (var i = 0; i < 1e6; i++) {
// Marshall JavaScript to Platform API
}
});
measure("Strings", () => {
const strings = [];
for (var i = 0; i < 100; i++) {
strings.push("abcdefghijklmnopqrstuvwxyz" + i);
}
for (var i = 0; i < 100000; i++) {
// Marshall JavaScript to Platform API
}
});
measure("Big data marshalling", () => {
const array = [];
for (var i = 0; i < 1 << 16; i++) {
array.push(i);
}
for (var i = 0; i < 200; i++) {
// Marshall JavaScript to Platform API
}
});
为了在 React Native 中使用 TestFixtures.{h,m}
,我们在项目中包含了 .h 和 .m 文件,然后编写了 JSI 来公开每个方法以便从 JavaScript 中使用。
这些基准测试的 JSI 设置如下
- (void)install:(jsi::Runtime &)runtime {
// initialize the test class
testFixtures = [[TestFixtures alloc] init];
// implement the JSI and expose `methodWithXYZ` to JavaScript
auto marshalMethodWithXYZHostFunction = [] (jsi::Runtime& _runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
int32_t result = [testFixtures methodWithX:(int32_t)arguments[0].asNumber() Y:(int32_t)arguments[1].asNumber() Z:(int32_t)arguments[2].asNumber()];
return jsi::Value(result);
};
auto marshalMethodWithXYZJsiFunction = jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "methodWithXYZ"), 3, marshalMethodWithXYZHostFunction);
runtime.global().setProperty(runtime, "methodWithXYZ", std::move(marshalMethodWithXYZJsiFunction));
// implement the JSI and expose `methodWithString` to JavaScript
auto marshalMethodWithStringHostFunction = [] (jsi::Runtime& _runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
NSString* result = [testFixtures methodWithString: [NSString stringWithUTF8String: arguments[0].asString(_runtime).utf8(_runtime).c_str()]];
return jsi::String::createFromUtf8(_runtime, [result UTF8String]);
};
auto marshalMethodWithStringJsiFunction = jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "methodWithString"), 1, marshalMethodWithStringHostFunction);
runtime.global().setProperty(runtime, "methodWithString", std::move(marshalMethodWithStringJsiFunction));
// implement the JSI and expose `methodWithBigData` to JavaScript
auto marshalMethodWithBigDataHostFunction = [] (jsi::Runtime& _runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
auto array = arguments[0].asObject(_runtime).asArray(_runtime);
NSMutableArray* nsArray = [NSMutableArray new];
size_t size = array.size(_runtime);
for (int i = 0; i < size; i++) {
[nsArray addObject: [NSNumber numberWithInt: (int32_t)array.getValueAtIndex(_runtime, i).asNumber()]];
}
[testFixtures methodWithBigData:nsArray];
return jsi::Value::undefined();
};
auto marshalMethodWithBigDataJsiFunction = jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "methodWithBigData"), 1, marshalMethodWithBigDataHostFunction);
runtime.global().setProperty(runtime, "methodWithBigData", std::move(marshalMethodWithBigDataJsiFunction));
}
我们现在可以在 React 代码中使用那些公开的方法
measure("Primitives", function () {
for (var i = 0; i < 1e6; i++) {
methodWithXYZ(i, i, i);
}
});
measure("Strings", () => {
const strings = [];
for (var i = 0; i < 100; i++) {
strings.push("abcdefghijklmnopqrstuvwxyz" + i);
}
for (var i = 0; i < 100000; i++) {
methodWithString(strings[i % strings.length]);
}
});
measure("Big data marshalling", () => {
const array = [];
for (var i = 0; i < 1 << 16; i++) {
array.push(i);
}
for (var i = 0; i < 200; i++) {
methodWithBigData(array);
}
});
为了在 NativeScript 中使用 TestFixtures.{h,m}
,我们将 .h 和 .m 文件放在 App_Resources/iOS/src
中,NativeScript CLI 将它们构建到我们的 Xcode 项目中并自动生成元数据,以便我们可以直接使用。
我们现在可以在 TypeScript 中直接使用 TestFixtures
const testFixtures = TestFixtures.alloc().init();
measure("Primitives", () => {
for (var i = 0; i < 1e6; i++) {
testFixtures.methodWithXYZ(i, i, i);
}
});
measure("Strings", () => {
const strings = [];
for (var i = 0; i < 100; i++) {
strings.push("abcdefghijklmnopqrstuvwxyz" + i);
}
for (var i = 0; i < 100000; i++) {
testFixtures.methodWithString(strings[i % strings.length]);
}
});
measure("Big data marshalling", () => {
const array = [];
for (var i = 0; i < 1 << 16; i++) {
array.push(i);
}
for (var i = 0; i < 200; i++) {
testFixtures.methodWithBigData(array);
}
});
测量函数
- (void)measurePerf:(NSString *)name action:(void (^)(void))action {
NSDate* startDate = [NSDate date];
action();
NSTimeInterval elapsedSeconds = -[startDate timeIntervalSinceNow];
NSLog(@"%@: %fms", name, elapsedSeconds * 1000);
}
为了在 Xcode 中使用 TestFixtures.{h,m}
,我们将 .h 和 .m 文件添加到 Xcode 项目中。
我们现在可以在 Xcode 项目中直接使用 TestFixtures
id instance = [[TestFixtures alloc] init];
[self measurePerf:@"Primitives" action: ^void() {
for (int32_t i = 0; i < 1e6; i++) {
[instance methodWithX:i Y:i Z:i];
}
}];
[self measurePerf:@"Strings" action: ^void() {
NSMutableArray* strings = [NSMutableArray array];
for (int32_t i = 0; i < 100; i++) {
[strings addObject:[NSString stringWithFormat:@"abcdefghijklmnopqrstuvwxyz%d", i]];
}
for (int32_t i = 0; i < 100000; i++) {
[instance methodWithString:strings[i % strings.count]];
}
}];
[self measurePerf:@"Big data marshalling" action: ^void() {
@autoreleasepool {
NSMutableArray* array = [NSMutableArray array];
for (int32_t i = 0; i < (1 << 16); i++) {
[array addObject:@(i)];
}
for (int32_t i = 0; i < 200; i++) {
[instance methodWithBigData:array];
}
}
}];
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 258 毫秒 | 49 毫秒 | 1010 毫秒 |
运行 2 | 262 毫秒 | 52 毫秒 | 1014 毫秒 |
运行 3 | 261 毫秒 | 54 毫秒 | 1012 毫秒 |
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 1359 毫秒 | 186 毫秒 | 812 毫秒 |
运行 2 | 1362 毫秒 | 189 毫秒 | 824 毫秒 |
运行 3 | 1359 毫秒 | 188 毫秒 | 815 毫秒 |
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 289 毫秒 | 57 毫秒 | 1052 毫秒 |
运行 2 | 298 毫秒 | 58 毫秒 | 1048 毫秒 |
运行 3 | 304 毫秒 | 59 毫秒 | 1061 毫秒 |
您可以通过以下步骤运行这些基准测试
git clone https://github.com/NativeScript/perf-metrics-universal-javascript.git
git checkout part-1
cd perf-metrics-universal-javascript/NativeScript
ns clean
ns run ios
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 1361 毫秒 | 215 毫秒 | 1427 毫秒 |
运行 2 | 1372 毫秒 | 211 毫秒 | 1421 毫秒 |
运行 3 | 1387 毫秒 | 212 毫秒 | 1429 毫秒 |
您可以通过以下步骤运行这些基准测试
git clone https://github.com/NativeScript/perf-metrics-universal-javascript.git
git checkout part-1
cd perf-metrics-universal-javascript/ReactNative
yarn
yarn ios
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 8 毫秒 | 14 毫秒 | 107 毫秒 |
运行 2 | 7 毫秒 | 17 毫秒 | 116 毫秒 |
运行 3 | 9 毫秒 | 16 毫秒 | 110 毫秒 |
您可以通过以下步骤运行这些基准测试
git clone https://github.com/NativeScript/perf-metrics-universal-javascript.git
git checkout part-1
cd perf-metrics-universal-javascript/NativeiOS
open NativeiOS.xcodeproj
您现在可以在 Xcode 中运行。
总结每个库中仅针对 String
处理的最佳数字
正如我们所见,JavaScript 在与平台 API 交互方面具有相当高的性能。
React Native 和 NativeScript 都可以在丰富的平台开发中实现出色的性能结果,这就是我们致力于共同发展这两个库的原因。
NativeScript 8.3 特别带来了接近纯平台 API 交互速度的优化,并且正在继续进一步优化。
最近的 NativeScript 8.3 版本 带来了约 30% 的性能优化增益。这可以通过使用上述相同的测试用例来衡量,例如
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 831 毫秒 | 121 毫秒 | 1240 毫秒 |
基本类型 | 字符串 | 大型数据编组 | |
---|---|---|---|
运行 1 | 258 毫秒 | 49 毫秒 | 1010 毫秒 |
Primitives
和 Strings
构成了应用程序中最常编组的数据类型,8.3 优化**改进了**所有三个类别的基准测试。
在 8.3 优化过程中,我们发现了其他可以改进的领域,以便在后续版本中进一步提升性能,并且我们始终专注于性能。
这些结果如何应用于您的情况的细微理解来自于您正在做的事情以及您想要的结果。编组速度有利于您在 JavaScript 代码和目标平台之间进行大量平台 API 交互的情况。
重要的是要了解 NativeScript 可以与 React Native 一起使用。我们计划在未来分享更多关于这方面的信息。
我们希望这些基准测试能帮助您在团队中讨论基准测试时有所启发,以及为什么我们继续享受其带来的成果。
[^1]: React Native JSI(JavaScript 接口)是一个有助于简化和加快 JavaScript 与原生平台之间通信的层。它是 React Native 与 Fabric UI 层和 Turbo 模块重新架构的核心元素。 - React Native JSI:第 1 部分 - 入门