返回博客首页
← 所有文章

(第一部分) 使用 JavaScript 进行通用平台开发的 iOS 性能基准测试

2022 年 9 月 1 日 — 作者:技术指导委员会 (TSC)

让我们开始了解使用 JavaScript 通用进行丰富平台开发的性能指标。

NativeScript TSC 与“React Native Avengers”、Margelo 联手,深入研究利用 JavaScript 进行丰富开发实践的各个方面。我们共同致力于不断发展、优化和丰富广阔的 JavaScript 生态系统。

  • → iOS 编组

未来探索的各种基准测试

  • iOS 启动时间、应用程序大小、导航、视图渲染
  • Android 编组基准测试
  • Android 启动时间、应用程序大小、导航、视图渲染
  • 看看典型的应用程序中进行了多少次 js < - > 本地调用
  • 多线程理解以及性能基准测试

JavaScript 已成为一种普遍流行的编程语言,因此它自然而然地被用于各种不同的平台开发,例如后端服务器基础设施以及桌面和移动客户端应用程序。

所有应用程序都以某种形式或方式涉及访问平台 API(例如绘制 UI、跟踪手势、访问摄像头等等)。大多数平台 API(Web 平台之外)通常不是用 JavaScript 编写的(例如,大多数 iOS API 都是用 Objective-C 和 Swift 编写的)。因此,为了继续在您选择的平台目标上使用 JavaScript,在语言之间交互时有一些指标需要了解。

这在实践中究竟有多大成本,需要进行深入透彻的分析才能正确理解。缺乏清晰且可重复的测试用例以及相应的基准测试导致犹豫使用 JavaScript 为原生平台构建应用程序。因此,我们 NativeScript 技术指导委员会有兴趣量化在多个性能指标上使用 JavaScript 进行原生应用程序开发的开销,以满足理解的需要。

一些预先考虑因素

  • 存在多个不同的 JS 运行时
  • 每个运行时都有多种访问原生平台 API 的可能方法
  • 有些可能比其他方法更优化

由于基准测试对于决策者理解的根本性,我们高度重视基准测试的共享,因此我们认为有必要重申

共享性能基准测试 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 中的基准测试实现

为了在 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 中的基准测试实现

为了在 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);
  }
});

Objective C 中的基准测试实现

测量函数

- (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];
        }
    }
}];

基准测试结果

发布模式(生产)

NativeScript 8.3 指标

  • iOS 15.5 iPhone 13 Pro 设备
基本类型 字符串 大型数据编组
运行 1 258 毫秒 49 毫秒 1010 毫秒
运行 2 262 毫秒 52 毫秒 1014 毫秒
运行 3 261 毫秒 54 毫秒 1012 毫秒

React Native 0.69.3 指标

  • iOS 15.5 iPhone 13 Pro 设备
基本类型 字符串 大型数据编组
运行 1 1359 毫秒 186 毫秒 812 毫秒
运行 2 1362 毫秒 189 毫秒 824 毫秒
运行 3 1359 毫秒 188 毫秒 815 毫秒

模拟器(调试)

NativeScript 8.3 指标

  • Xcode 13.4,iOS 15.5 iPhone 13 Pro 模拟器
  • 在配备 64 GB 内存的 Mac M1 Max(macOS 12.3.1)上运行
基本类型 字符串 大型数据编组
运行 1 289 毫秒 57 毫秒 1052 毫秒
运行 2 298 毫秒 58 毫秒 1048 毫秒
运行 3 304 毫秒 59 毫秒 1061 毫秒
自己运行 NativeScript 基准测试

您可以通过以下步骤运行这些基准测试

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

React Native 0.69.3 指标

  • Xcode 13.4,iOS 15.5 iPhone 13 Pro 模拟器
  • 在配备 64 GB 内存的 Mac M1 Max(macOS 12.3.1)上运行
基本类型 字符串 大型数据编组
运行 1 1361 毫秒 215 毫秒 1427 毫秒
运行 2 1372 毫秒 211 毫秒 1421 毫秒
运行 3 1387 毫秒 212 毫秒 1429 毫秒
自己运行 React Native 基准测试

您可以通过以下步骤运行这些基准测试

git clone https://github.com/NativeScript/perf-metrics-universal-javascript.git
git checkout part-1
cd perf-metrics-universal-javascript/ReactNative

yarn
yarn ios

Objective C 指标

  • Xcode 13.4,iOS 15.5 iPhone 13 Pro 模拟器
  • 在配备 64 GB 内存的 Mac M1 Max(macOS 12.3.1)上运行
基本类型 字符串 大型数据编组
运行 1 8 毫秒 14 毫秒 107 毫秒
运行 2 7 毫秒 17 毫秒 116 毫秒
运行 3 9 毫秒 16 毫秒 110 毫秒
自己运行 Objective C 基准测试

您可以通过以下步骤运行这些基准测试

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 处理的最佳数字

  • NativeScript:49 毫秒
  • React Native:186 毫秒
  • Objective C:14 毫秒

正如我们所见,JavaScript 在与平台 API 交互方面具有相当高的性能。

React Native 和 NativeScript 都可以在丰富的平台开发中实现出色的性能结果,这就是我们致力于共同发展这两个库的原因。

NativeScript 8.3 特别带来了接近纯平台 API 交互速度的优化,并且正在继续进一步优化。

NativeScript 对性能的执着

最近的 NativeScript 8.3 版本 带来了约 30% 的性能优化增益。这可以通过使用上述相同的测试用例来衡量,例如

8.2 基准测试

基本类型 字符串 大型数据编组
运行 1 831 毫秒 121 毫秒 1240 毫秒

8.3 基准测试

基本类型 字符串 大型数据编组
运行 1 258 毫秒 49 毫秒 1010 毫秒

PrimitivesStrings 构成了应用程序中最常编组的数据类型,8.3 优化**改进了**所有三个类别的基准测试。

在 8.3 优化过程中,我们发现了其他可以改进的领域,以便在后续版本中进一步提升性能,并且我们始终专注于性能。

观察和细微的理解

这些结果如何应用于您的情况的细微理解来自于您正在做的事情以及您想要的结果。编组速度有利于您在 JavaScript 代码和目标平台之间进行大量平台 API 交互的情况。

重要的是要了解 NativeScript 可以与 React Native 一起使用。我们计划在未来分享更多关于这方面的信息。

我们希望这些基准测试能帮助您在团队中讨论基准测试时有所启发,以及为什么我们继续享受其带来的成果。

[^1]: React Native JSI(JavaScript 接口)是一个有助于简化和加快 JavaScript 与原生平台之间通信的层。它是 React Native 与 Fabric UI 层和 Turbo 模块重新架构的核心元素。 - React Native JSI:第 1 部分 - 入门