JavaScript to Native Communication in React Native
Frameworks like React Native enable JavaScript code to control native platform features on iOS and Android by bridging two distinct runtime environments. This article explains the mechanisms behind this communication, detailing the traditional JSON-based asynchronous bridge, the transition to the modern JavaScript Interface (JSI), and how these systems coordinate JavaScript threads with native platform threads to execute native APIs and render user interfaces.
The Dual-Runtime Architecture
React Native applications execute in two primary environments running side-by-side:
- The JavaScript Runtime: Executes application business logic using an engine such as Hermes, V8, or JavaScriptCore on a dedicated JavaScript thread.
- The Native Platform: Executes platform-specific code (Objective-C/Swift on iOS, Java/Kotlin on Android) on the main UI thread and background native threads.
Because these two runtimes use completely different memory spaces and instruction sets, direct function calls between them require a specialized communication layer.
The Legacy Approach: The React Native Bridge
Historically, React Native relied on a centralized, asynchronous message-passing system known as The Bridge.
How the Bridge Works
- Serialization: When JavaScript invokes a native API (such as accessing device storage), the function name, arguments, and callback IDs are converted into a stringified JSON payload.
- Queuing and Batching: The JSON payload is placed into a message queue to minimize cross-thread overhead.
- Transmission: The message is passed across the bridge via a C++ transport layer to the native environment.
- Deserialization and Execution: The native platform decodes the JSON, identifies the requested module and method, executes the task on a native background thread, and serializes the result back to JavaScript via the same process.
Limitations of the Bridge
- Serialization Overhead: Converting complex objects to and from JSON consumes CPU cycles and increases latency.
- Asynchronous-Only: JavaScript cannot invoke native code synchronously, making synchronized UI updates or real-time measurements prone to layout jumps and frame drops.
The Modern Architecture: JavaScript Interface (JSI)
Modern React Native replaces the serialized bridge with the JavaScript Interface (JSI), a lightweight, general-purpose C++ layer that allows the JavaScript engine to interact directly with native code.
Direct Host Object References
JSI exposes native C++ objects directly to the JavaScript runtime as HostObjects.
Instead of sending serialized messages through a pipe: * Native
modules are implemented in C++ or wrapped in a C++ interface. *
JavaScript holds a direct memory reference to the C++ HostObject. *
JavaScript can invoke native methods directly using standard JavaScript
function call syntax (global.nativeModule.method()).
Key Benefits of JSI
- Zero Serialization: Data is shared directly through typed C++ data structures without converting to JSON strings.
- Synchronous and Asynchronous Execution: JavaScript can invoke native methods synchronously when immediate results are required (e.g., rendering layout calculations) or asynchronously for heavy background operations.
- Engine Agnostic: JSI abstracts the underlying JavaScript engine, allowing seamless switching between Hermes, V8, or JavaScriptCore without altering the native communication layer.
TurboModules and Fabric: JSI in Practice
The modern communication pipeline relies on two systems built on top of JSI:
- TurboModules: The native module system that replaces bridge-based native modules. TurboModules are loaded lazily—meaning native modules are only initialized when JavaScript actively requests them, significantly reducing app startup times.
- Fabric: The modern rendering engine. Fabric allows the JavaScript UI layout engine to create and manipulate native UI components directly via C++ HostObjects, ensuring UI state remains consistent across both native and JavaScript execution contexts without queue-based lag.