JSI: Writing Direct C++ Bindings Without the Bridge
AI generated
RN
native
React Native / New Architecture
JSI
Writing direct, synchronous C++ bindings without the bridge

The JavaScript Interface, JSI, connects the JavaScript runtime and native C++ code directly, without JSON serialization or an asynchronous bridge round trip. Building a HostObject shows how synchronous native bindings work in practice, why the performance gain matters mainly for frequent calls, and where the limits of this approach lie.

10 min read JSI HostObject C++ Bindings

1. Why the classic bridge was too slow for many use cases

In the original React Native architecture, every call between JavaScript and native code ran through the bridge, an asynchronous channel that serialized each call including its arguments into a JSON-compatible format, batched it and sent it across a message queue to the respective other thread. For rare calls like a one-off API request that was unproblematic, but for functions called hundreds of times per second, for example animation values or sensor data, the overhead from serialization, queueing and thread switching added up noticeably.

A second structural problem was that the bridge was fundamentally asynchronous. Even a trivial, synchronous computation on the native side had to be modeled as a promise or callback, which added unnecessary complexity to JavaScript code and made real synchronous APIs, as native platform SDKs often offer, practically impossible without resorting to risky workarounds like synchronous methods on NativeModules, which only worked in a limited way on Android anyway.

2. What JSI really is: a C++ interface between runtime and native code

The JavaScript Interface, JSI for short, is not a new bridge but a C++ abstraction layer that lets native code talk directly to the JavaScript runtime, whether that runtime is Hermes, JavaScriptCore or another JSI-compatible engine. Instead of serializing data, JSI keeps references to actual JavaScript objects and functions in memory and lets native C++ code read, mutate or call these objects directly, the same way JavaScript itself would.

That makes synchronous calls structurally possible for the first time: a C++ function can be called from JavaScript and immediately return a value, with no promise, callback or bridge round trip at all. That is the technical foundation both TurboModules and Fabric are built on, but JSI can also be used completely independently to write custom, high-performance native bindings.

3. HostObject: exposing your own objects directly in the JS runtime

The central building block for custom JSI bindings is the jsi::HostObject class. Subclassing it and overriding the get and set methods lets you attach a C++ object into the JavaScript runtime that behaves like a normal object to JavaScript code, but whose properties and methods actually execute in C++. Every property access from JavaScript synchronously calls the corresponding C++ method.

This suits cases where native computation or native state needs to be available from JavaScript with no noticeable latency, for example a cryptography module, an image processing kernel, or a high-performance data structure like a ring buffer for sensor data. Unlike a classic native module, no additional bridge call is incurred per access.

4. A practical example: a synchronous HostObject implementation

A simple example is a counter that lives entirely in C++ and can be synchronously read and incremented from JavaScript. The constructor implements a get method that reacts to the requested property name, plus optionally a set method if write access should be allowed. Installation happens through a global property on the jsi::Runtime object, usually at app startup.

The example below shows a minimal HostObject class with a readable property and a callable method, both implemented directly in C++, with no bridge call in the background at all.


class CounterHostObject : public jsi::HostObject {
public:
  explicit CounterHostObject(int start) : value_(start) {}

  jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override {
    auto propName = name.utf8(rt);
    if (propName == "value") {
      return jsi::Value(value_);
    }
    if (propName == "increment") {
      return jsi::Function::createFromHostFunction(
          rt, name, 0,
          [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) {
            value_ += 1;
            return jsi::Value(value_);
          });
    }
    return jsi::Value::undefined();
  }

private:
  int value_;
};

// Installed once at app startup, e.g. in a custom JSI installer
void installCounter(jsi::Runtime &runtime) {
  auto counter = std::make_shared<CounterHostObject>(0);
  runtime.global().setProperty(
      runtime, "NativeCounter",
      jsi::Object::createFromHostObject(runtime, counter));
}

5. Installation: registering JSI modules through TurboModuleManager or a custom installer

For production projects it pays off to register a custom JSI module either through the existing TurboModule mechanism, if it fits into the regular module system, or through a dedicated, minimal installer that runs directly when the JavaScript runtime is created. The latter is common for very early, cross-platform low-level bindings, for example logging or performance measurement, that need to be available before the actual JavaScript bundle even loads.

It is important to hook the installer into both the iOS and the Android runtime initialization, since both platforms have separate entry points for runtime creation. Forgetting one platform produces an undefined error on the first access to the global object, which is hard to debug without knowing the installation order.

6. Memory management: shared_ptr, runtime lifecycle and GC interop

JSI objects live in two worlds at once: as C++ objects with classic, reference-counted lifetime through std::shared_ptr, and as values managed by the JavaScript garbage collector inside the runtime. A HostObject stays alive as long as at least one JavaScript reference to it exists, which means cyclic references between HostObjects and JavaScript functions can leak memory unless weak references are used deliberately.

A common mistake is capturing a this reference by copy instead of through a shared_ptr inside a host function lambda, which can leave the underlying C++ object already destroyed while JavaScript still holds a valid reference to it. Such bugs usually show up as sporadic crashes far away from the actual source of the error and are hard to reproduce without a clean ownership structure.

7. Performance in practice: bridge versus JSI for frequent calls

The performance difference between bridge and JSI shows up most clearly for functions called very frequently with small amounts of data. A bridge call, regardless of the actual payload, always costs the overhead of JSON serialization, queueing and at least one thread switch, while a JSI call through a HostObject only costs the actual function execution, since no data format needs to be converted.

At thousands of calls per second, for example an animation loop querying several native values per frame, that is the difference between a smooth 60 hertz animation and noticeable jank. For rare calls, say a one-off file access, the difference is barely measurable, which is why JSI pays off mainly for hot-path code, not for every native binding.

8. Limits of JSI: thread safety and when async still makes sense

JSI calls run synchronously on the thread they are called from, usually the JavaScript thread. That means a long-running operation inside a HostObject blocks the entire JavaScript thread and freezes the UI if triggered from the main thread. For compute-heavy tasks like large-scale image processing, a dedicated background thread with an asynchronous callback remains the right choice, not a direct synchronous JSI call.

In addition, access to a jsi::Runtime object itself is not thread-safe, since the runtime is designed for exactly one thread. Anyone wanting to return values to JavaScript from a background thread must make that switch explicitly through a mechanism intended for it, such as runOnJSQueueThread, instead of addressing the runtime directly from an arbitrary thread.

9. Debugging JSI code: native crashes and stack traces

Bugs in JSI code often do not show up as a normal JavaScript exception but as a native crash with a C++ stack trace that is hard to read without symbolication. On iOS, a debugger attached through Xcode with debug symbols enabled helps, on Android the native crash handler combined with ndk-stack or symbolication through Play Console crash reports produces usable stack traces.

A typical class of bugs is invalid type conversion, for example trying to read a JSI value as a number when JavaScript actually passed undefined. Since JSI deliberately offers little automatic error handling to avoid overhead, it pays off to consistently check the actual value type at the boundaries of a custom HostObject before processing it further, instead of relying on an implicit exception.

Criterion Classic bridge JSI HostObject Practical consequence
Call type Exclusively asynchronous Synchronous calls possible Immediate return values without a promise
Data transfer JSON serialization Direct object references No conversion overhead
Threading Bridge queue on a separate thread Runs on the calling thread Long operations can block the UI if misused
Type safety Loose, runtime based Explicit jsi::Value type checks needed Developer must guard types themselves
Use case Rare, non-critical calls High-frequency hot-path calls JSI is not worth it for every binding

Mironsoft

React Native app development and Magento integration

A mobile app for the Magento shop that actually runs smoothly?

We build React Native apps cleanly connected to the Magento REST or GraphQL API, from the first line of code to publishing on the App Store and Google Play.

App Concept

Plan the architecture and feature scope of a Magento-connected app together.

Magento API Integration

Cleanly connect product catalog, cart, and checkout to the shop API.

Store Publishing

Guide the App Store and Google Play release process without pitfalls.

10. Summary

JSI Direct Bindings at a Glance

JSI at its core

A C++ interface that connects the JavaScript runtime and native code directly, without serialization.

HostObject

Lets custom C++ objects behave like normal JavaScript objects with synchronous methods.

Performance

Pays off mainly for very frequent calls with small payloads, barely measurable for rare calls.

Limits

Synchronous calls block the calling thread, long-running work still belongs on a background thread.

11. FAQ: JSI Direct Bindings at a Glance

1Is JSI a new version of the bridge?
No, JSI fundamentally replaces the bridge concept with direct C++ references to the JavaScript runtime instead of serializing data and sending it through a queue.
2Can I use JSI with Hermes and JavaScriptCore alike?
Yes, JSI is designed as an abstraction layer over the underlying engine, so as long as the engine implements the JSI interface, the same C++ code works with Hermes and JavaScriptCore.
3What is the difference between a native module and a HostObject?
A classic native module usually runs over the bridge or TurboModules with defined method signatures, while a HostObject is a flexible object attached directly into the runtime that can respond synchronously to arbitrary property access.
4Does a synchronous JSI call always block the UI?
Only if it runs for a long time on the JavaScript thread, since that thread is also responsible for UI-relevant updates. Compute-heavy work should still be offloaded to a background thread.
5How do I correctly install a custom JSI module?
Through an installer that runs when the JavaScript runtime is created, on both iOS and Android, usually via a global property on the runtime object.
6Why does my app crash sporadically on a HostObject access?
Often because of faulty ownership, for example when a HostObject is destroyed while JavaScript still holds a valid reference to it. Consistent use of shared_ptr prevents this.
7Do I need JSI for every native feature?
No, for rare or non-critical calls a classic TurboModule is entirely sufficient. JSI HostObjects pay off mainly for high-frequency, performance-critical hot-path calls.
8Can I access the JS runtime directly from a background thread?
Not directly, since jsi::Runtime is not thread-safe. Access must be moved back to the JavaScript thread through a mechanism designed for that.
9How do I find the cause of a native JSI crash?
Through a native debugger attached to the runtime with debug symbols, via Xcode on iOS, via ndk-stack or symbolicated crash reports on Android.
10Does JSI replace TurboModules and Fabric?
No, JSI is the shared technical foundation both TurboModules and Fabric are built on, but it can also be used independently for custom, direct bindings.