Can React Native Close the Performance Gap? A Dive into C++ Turbo Native Modules

codeherence
9 min readApr 13, 2023

React Native has become a powerful and valuable tool for individuals and companies to develop cross-platform applications. However, one common pitfall of the technology is its performance when compared to native applications. This is especially true when it comes to compute-heavy tasks that need to be run on the JavaScript thread, such as compression, file IO, and streaming.

The React Native team at Meta recognized the technology’s performance drawbacks and developed a plan to restructure the framework’s architecture entirely — giving birth to Turbo Modules and Fabric.

In this blog, we are going to dive into C++ Turbo Native Modules and compare the performance against JavaScript. Namely, we will write a simple React Native application that generates the n-th number in the Fibonacci sequence using the naive recursive implementation to simulate a compute-heavy task. For more information on how this algorithm works, see here.

Results

Before running through the setup and implementation, I want to post the results first. Keep in mind that these results are from running the application in development mode, directly from my laptop.

After implementing the Fibonacci algorithm in C++ and JavaScript and running the algorithm on our application, we got the following results from the iOS simulator:

n=20  cpp=0ms    js=3ms
n=22 cpp=0ms js=4ms
n=25 cpp=1ms js=19ms
n=28 cpp=3ms js=75ms
n=30 cpp=6ms js=198ms
n=36 cpp=106ms js=3742ms
n=38 cpp=281ms js=9353ms

and on a physical Google 7 Pixel:

n=20  cpp=0ms    js=2ms
n=22 cpp=0ms js=7ms
n=25 cpp=3ms js=21ms
n=28 cpp=3ms js=84ms
n=30 cpp=7ms js=154ms
n=36 cpp=124ms js=2761ms
n=38 cpp=329ms js=7878ms

where n is the n-th number in the Fibonacci sequence, cpp is the duration it took our Turbo Module to compute it, and js is the duration it took our regular JavaScript implementation to compute it. Keep in mind that the two implementations are equivalent.

From the data above, we can see that we have improved the performance by at least an order of magnitude (sometimes up to 40x) by simply letting C++ do the work for us.

Skip the Setup and Implementation

If you would like to just clone the repository and run the tests on your device/simulators, here is how you can do it:

Clone the repository at https://github.com/e-younan/CxxTurboModulesGuide.

Then install the project dependencies by running the following:

cd CxxTurboModulesGuide
yarn install

Then install the pod dependencies:

cd ios && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install && cd ..

Lastly, simply run the application by running yarn ios or yarn android.

Setup

For the setup, we followed the C++ Turbo Native Modules guide on the React Native website. The setup below is copied from there to help you get set up. We begin by creating the project:

npx react-native init CxxTurboModulesGuide
cd CxxTurboModulesGuide
yarn install

On Android, we enable the New Architecture by modifying the android/gradle.properties file:

newArchEnabled=true

On iOS enable the New Architecture when running pod install in the ios folder:

RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

Now that we have the bare application set up, we can proceed with writing our Turbo Module that computes the n-th number in the Fibonacci sequence.

Turbo Module

In the root directory, we create a folder tm that will contain your C++ Turbo Modules for your application.

1. JavaScript Specification

Inside the tm folder, create a file NativeSampleModule.ts which will hold the spec of our Turbo Modules. Inside the file, paste the following:

import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
readonly doFibExpensive: (n: number) => number;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeSampleModule');

2. Codegen Configuration

We need to add some configuration for Codegen.

Application

Update your app’s package.json file with the following entries:

{
// ...
"description": "React Native with Cxx Turbo Native Modules",
"author": "<Evan Younan> <me@eyounan.com> (https://github.com/e-younan)",
"license": "MIT",
"homepage": "https://github.com/e-younan/#readme",
// ...
"codegenConfig": {
"name": "AppSpecs",
"type": "all",
"jsSrcsDir": "tm",
"android": {
"javaPackageName": "com.facebook.fbreact.specs"
}
}
}

It adds necessary properties which we will later re-use in the iOS podspec file and configures Codegen to search for specs inside the tm folder.

iOS: Create the podspec file

For iOS, you’ll need to create a AppTurboModules.podspec file in the tm folder - which will look like:

require "json"

package = JSON.parse(File.read(File.join(__dir__, "../package.json")))

Pod::Spec.new do |s|
s.name = "AppTurboModules"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "12.4" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }
s.source_files = "**/*.{h,cpp}"
s.pod_target_xcconfig = {
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
install_modules_dependencies(s)
end

and we modify our Podfile to recognize the module as a dependency, e.g., after the use_react_native!(...) section:

  # ...

use_react_native!(
:path => config[:reactNativePath],
# Hermes is now enabled by default. Disable by setting this flag to false.
# Upcoming versions of React Native may rely on get_default_flags(), but
# we make it explicit here to aid in the React Native upgrade process.
:hermes_enabled => flags[:hermes_enabled],
:fabric_enabled => flags[:fabric_enabled],
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
:flipper_configuration => flipper_config,
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)

target 'CxxTurboModulesGuideTests' do
inherit! :complete
# Pods for testing
end

# To register our Turbo Module
if ENV['RCT_NEW_ARCH_ENABLED'] == '1'
pod 'AppTurboModules', :path => "./../tm"
end

# ...

Android: build.gradle, CMakeLists.txt, Onload.cpp

For Android, you’ll need to create a CMakeLists.txt file in the tm folder - which will look like:

cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

add_compile_options(
-fexceptions
-frtti
-std=c++17)

file(GLOB tm_SRC CONFIGURE_DEPENDS *.cpp)
add_library(tm STATIC ${tm_SRC})

target_include_directories(tm PUBLIC .)
target_include_directories(react_codegen_AppSpecs PUBLIC .)

target_link_libraries(tm
jsi
react_nativemodule_core
react_codegen_AppSpecs)

It defines the tm folder as a source for native code and sets up necessary dependencies.

You need to add it as a dependency to your application in android/app/build.gradle, e.g., at the very end of that file:

android {
// ...

// Add this block
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
}
}

// ...
}

3. Module Registration

iOS

To register a C++ Turbo Native Module in your app you will need to update ios/CxxTurboModulesGuide/AppDelegate.mm . Let’s replace the contents of AppDelegate.mm with the following:

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/CoreModulesPlugins.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <NativeSampleModule.h>

@interface AppDelegate () <RCTTurboModuleManagerDelegate> {}
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"CxxTurboModulesGuide";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};

return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
///
/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`.
- (BOOL)concurrentRootEnabled
{
return true;
}

#pragma mark RCTTurboModuleManagerDelegate

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
if (name == "NativeSampleModule") {
return std::make_shared<facebook::react::NativeSampleModule>(jsInvoker);
}
return nullptr;
}

@end

This will instantiante a NativeSampleModule associated with the name NativeSampleModule as defined in our JavaScript spec file earlier.

Android

Android apps aren’t setup for native code compilation by default. I have provided you a set of convenience commands to create the necessary folders/files. Run the following from the root folder:

cd android/app/src/main
mkdir jni
cd ../../../..
cp node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup/CMakeLists.txt android/app/src/main/jni/
cp node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup/OnLoad.cpp android/app/src/main/jni/

Manual:

If the commands above did not work for you, feel free to do the following manually:

1.) Create the folder android/app/src/main/jni

2.) Copy CMakeLists.txt and Onload.cpp from node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup into the android/app/src/main/jni folder.

Now, we update OnLoad.cpp with the following (i.e., add the lines with the starting with a + and remove the +):

// ...

#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <rncli.h>
+ #include <NativeSampleModule.h>

// ...

std::shared_ptr<TurboModule> cxxModuleProvider(
const std::string &name,
const std::shared_ptr<CallInvoker> &jsInvoker) {
+ if (name == "NativeSampleModule") {
+ return std::make_shared<facebook::react::NativeSampleModule>(jsInvoker);
+ }
return nullptr;
}

// ...

and update CMakeLists.txt in the jni folder with the following entries, e.g., at the very end of that file (remove the +’s):

// ...

# This file includes all the necessary to let you build your application with the New Architecture.
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

+ # App needs to add and link against tm (TurboModules) folder
+ add_subdirectory(${REACT_ANDROID_DIR}/../../../tm/ tm_build)
+ target_link_libraries(${CMAKE_PROJECT_NAME} tm)

This will instantiate a NativeSampleModule associated with the name NativeSampleModule as defined in our JavaScript spec file earlier.

4. C++ Native Code

For the final step, we need to connect the JavaScript side to the native platforms. This involves two steps:

  • Run Codegen to see what it generates
  • Write your native code, implementing the generated interfaces.

On iOS Codegen is run each time you execute in the ios folder:

RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

On Android Codegen is run each time you execute:

yarn android

Keep in mind that you need to re-run Codegen if you have changed your JavaScript spec.

Implementation

Now create a NativeSampleModule.h file in the tm folder with the following content:

#pragma once

#if __has_include(<React-Codegen/AppSpecsJSI.h>) // CocoaPod headers on Apple
#include <React-Codegen/AppSpecsJSI.h>
#elif __has_include("AppSpecsJSI.h") // CMake headers on Android
#include "AppSpecsJSI.h"
#endif
#include <memory>
#include <string>

namespace facebook::react
{

class NativeSampleModule : public NativeSampleModuleCxxSpec<NativeSampleModule>
{
public:
NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker);

int doFibExpensive(jsi::Runtime &rt, int n);
};

} // namespace facebook::react

And now, we add a NativeSampleModule.cpp file in the tm folder with an implementation of the recursive Fibonacci for it:

#include <fstream>
#include "NativeSampleModule.h"

namespace facebook::react
{

NativeSampleModule::NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker)
: NativeSampleModuleCxxSpec(std::move(jsInvoker)) {}

int NativeSampleModule::doFibExpensive(jsi::Runtime &rt, int n)
{
if (n < 2)
{
return n;
}
else
{
return doFibExpensive(rt, n - 1) + doFibExpensive(rt, n - 2);
}
}
} // namespace facebook::react

And lastly, since we have added new C++ files, we run the following in the ios folder again:

RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

5. Adding the C++ Turbo Native Module to the App

Keep in mind that the App.tsx files should exist in the root folder. Replace your App.tsx with the following:

import React, {useState} from 'react';
import {
SafeAreaView,
Text,
StyleSheet,
View,
ScrollView,
Button,
} from 'react-native';
import NativeSampleModule from './tm/NativeSampleModule';

const TESTS = [20, 22, 25, 28, 30, 36, 38];

const doJsFib = (n: number): number => {
if (n < 2) {
return n;
}
return doJsFib(n - 1) + doJsFib(n - 2);
};

const doFibTest = async (n: number) => {
// Wait 100ms before starting the test to allow the app to settle and render new results
await new Promise(res => setTimeout(() => res(null), 100));

let before = Date.now();
// C++
NativeSampleModule.doFibExpensive(n);
const duration = Date.now() - before;

before = Date.now();
// JS
doJsFib(n);
const duration2 = Date.now() - before;

return [duration, duration2] as const;
};

interface FibonacciTestResult {
n: number;
cppDuration: number;
jsDuration: number;
}

export default function App(): JSX.Element {
const [running, setRunning] = useState(false);
// Store the results of the tests
const [results, setResults] = useState<FibonacciTestResult[]>([]);

const onPressStart = async () => {
if (running) {
return;
}

setRunning(true);

for await (const n of TESTS) {
const [cppDuration, jsDuration] = await doFibTest(n);
setResults(prev => [...prev, {n, cppDuration, jsDuration}]);
}

setRunning(false);
};

return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollStyle}>
<Text style={styles.title}>C++ vs JS Fibonacci Test</Text>
<Button
disabled={running}
title={running ? 'Running - please wait...' : 'Start test'}
onPress={onPressStart}
/>

<View style={styles.titleRow}>
<Text style={styles.boldText}>n</Text>
<Text style={styles.boldText}>C++</Text>
<Text style={styles.boldText}>JS</Text>
</View>

<View style={styles.resultsContainer}>
{results.map((test, i) => (
<View style={styles.resultRow} key={`test-${i}`}>
<Text style={styles.text}>{test.n}</Text>
<Text style={styles.text}>{test.cppDuration}ms</Text>
<Text style={styles.text}>{test.jsDuration}ms</Text>
</View>
))}
</View>
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
scrollStyle: {
flex: 1,
},
button: {
backgroundColor: 'lightblue',
padding: 10,
justifyContent: 'center',
alignItems: 'center',
},
resultsContainer: {
marginTop: 20,
gap: 8,
},
titleRow: {
marginTop: 20,
width: '100%',
alignItems: 'center',
flexDirection: 'row',
},
resultRow: {
width: '100%',
alignItems: 'center',
flexDirection: 'row',
},
text: {
flex: 1,
textAlign: 'center',
},
boldText: {
flex: 1,
fontWeight: 'bold',
textAlign: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginVertical: 12,
textAlign: 'center',
},
});

What you should notice is the function doFibTest will run the algorithm using our C++ Turbo Module and a JavaScript equivalent, and store the results of the computations (i.e., how long each took in milliseconds) to display on the UI. If you would like to review the performance differences between the C++ and JavaScript implementation, they are posted above in the Results section.

Conclusion

It is clear that with the introduction of Turbo Modules, applications that require heavy computation can offload the work to C++ and increase performance drastically. Some libraries have already migrated and are taking advantage of the new benefits C++ Turbo Modules have introduced into the ecosystem — check out react-native-mmkv, for example.

Since the re-architecture of React Native which introduced Turbo Modules and Fabric, the performance gap between applications developed using it and native applications has started closing.

Get in touch

If you or your organization are seeking to develop highly interactive and high-performance cross-platform applications, we invite you to connect with us and explore how our expertise can benefit your project.

Follow me

If you would like to see more content like this, make requests on the type of content you want to see, etc., feel free to:

  1. Follow my Twitter
  2. Join my Discord
  3. Follow me on LinkedIn

In the upcoming weeks, I will be working on creating libraries that take advantage of C++ Turbo Modules to improve the performance of specific use cases that are common in React Native applications. Stay tuned!

--

--