Skill: Fast Native Modules
Build performant Turbo Modules using modern languages and background threading.
Quick Pattern
Incorrect (sync method blocks JS thread):
@objc func heavyWork() -> NSNumber {
Thread.sleep(forTimeInterval: 2) // Blocks JS for 2s!
return 42
}Correct (async on background thread):
@objc func heavyWork(
resolve: @escaping RCTPromiseResolveBlock,
reject: RCTPromiseRejectBlock
) {
DispatchQueue.global().async {
let result = self.compute()
resolve(result)
}
}When to Use
- Creating new native modules
- Optimizing existing module performance
- Heavy computation needs to run off JS thread
- Cross-platform C++ code needed
Prerequisites
- React Native Builder Bob for scaffolding
npx create-react-native-library@latest my-libraryStep-by-Step Instructions
1. Scaffold with Builder Bob
npx create-react-native-library@latest awesome-library
# Follow prompts: choose Turbo Module, select languagesCreates ready-to-publish library with:
- iOS (Obj-C/Swift) support
- Android (Kotlin) support
- TypeScript definitions
- Codegen setup
For local modules:
npx create-react-native-library@latest awesome-library --local2. Run on Background Thread (iOS)
@objc func heavyOperation(
_ input: Double,
resolve: @escaping RCTPromiseResolveBlock,
reject: RCTPromiseRejectBlock
) {
DispatchQueue.global().async {
// Heavy work on background thread
let result = self.expensiveComputation(input)
resolve(result)
}
}3. Run on Background Thread (Android)
class AwesomeLibraryModule(reactContext: ReactApplicationContext) :
NativeAwesomeLibrarySpec(reactContext) {
private val moduleScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun heavyOperation(input: Double, promise: Promise?) {
moduleScope.launch {
// Heavy work on coroutine
val result = expensiveComputation(input)
promise?.resolve(result)
}
}
override fun invalidate() {
super.invalidate()
moduleScope.cancel() // Prevent memory leaks!
}
}Use structured concurrency: keep a module-owned CoroutineScope, cancel it in invalidate(), avoid GlobalScope.launch, use SupervisorJob so one failed operation does not cancel unrelated in-flight work, and choose Dispatchers.Default for CPU work or Dispatchers.IO for disk/network/database work.
4. Use C++ for Cross-Platform Code
Create C++ Turbo Module for shared logic:
// MyCppModule.h
#pragma once
#include <ReactCommon/TurboModule.h>
namespace facebook::react {
class MyCppModule : public TurboModule {
public:
MyCppModule(std::shared_ptr<CallInvoker> jsInvoker);
double multiply(double a, double b);
};
} // namespace facebook::reactFollow the registration mechanism documented for the React Native version you target. Avoid copying old +load registration workarounds unless current RN docs or template output still require them.
Threading Summary
| Method Type | Default Thread | Best Practice |
|---|---|---|
| Sync | JS thread | Keep fast (<16ms) |
| Async | Native modules thread | OK for moderate work |
| Heavy async | Custom background | Use DispatchQueue/Coroutines |
Language Interop Costs
| Interface | Overhead | Notes |
|---|---|---|
| Obj-C / Obj-C++ ↔ C++ | Low | Common iOS interop path |
| Swift ↔ C++ | Version-dependent | Verify supported Swift/Xcode/RN setup |
| Kotlin ↔ C++ (JNI) | Higher | Batch calls and avoid per-item crossings |
| C++ Turbo Module | Low | JSI direct access when correctly registered |
Tip: C++ Turbo Modules skip JNI at runtime since JS holds direct C++ function references via JSI.
Code Example: Complete Async Operation
// TypeScript interface
export interface Spec extends TurboModule {
multiply(a: number, b: number): number; // Sync
heavyOperation(input: number): Promise<number>; // Async
}// Android implementation
override fun heavyOperation(input: Double, promise: Promise?) {
moduleScope.launch {
try {
val result = withContext(Dispatchers.Default) {
// Simulate heavy work
delay(1000)
input * 2
}
promise?.resolve(result)
} catch (e: Exception) {
promise?.reject("ERROR", e.message)
}
}
}// iOS implementation
@objc func heavyOperation(
_ input: Double,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
DispatchQueue.global(qos: .userInitiated).async {
// Simulate heavy work
Thread.sleep(forTimeInterval: 1.0)
let result = input * 2
resolve(result)
}
}Common Pitfalls
- Sync methods that block: Keep sync methods trivial and deterministic; make anything that can block, allocate heavily, perform I/O, or wait on locks async/background work
- Forgetting to cancel coroutine scope: Causes memory leaks
- Not handling errors in async: Always try/catch with reject
- Accessing UI from background: Dispatch to main thread
Related Skills
- native-threading-model.md - Thread details
- native-memory-patterns.md - Memory in native code