skip to content

JSI & the Legacy Bridge

JSI lets JavaScript hold C++ host objects and call host functions synchronously, replacing a bridge that sent batched JSON messages asynchronously. Interviewers ask why that swap mattered.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In React Native, what did JSI change compared with the legacy bridge for calls between JavaScript and native code?

level: middleimportance: must knowfreq 75%

answer

  1. messages versus direct calls
  2. no JSON in between
  3. return values without callbacks
  4. JS holds a C++ object
  5. host functions and HostObject

basics

~20 s

The legacy bridge sent serialized, batched messages asynchronously, so native calls normally answered through callbacks or promises. JSI lets JavaScript call C++ host functions synchronously and hold references to C++ host objects, with no serialization step.

solid answer

~40 s

The **legacy bridge** was a message channel: JavaScript queued each native call as a module id, a method id and JSON-compatible arguments, the queue was flushed in batches, and native code answered later through a callback or a promise. **JSI** is a C++ interface to the JavaScript engine (`jsi::Runtime`) that lets native code expose **host functions** and **host objects** to JavaScript. JavaScript calls them directly and synchronously, gets a return value, and can hold a reference to a native object instead of a serialized copy. Turbo Native Modules and the Fabric renderer are built on it, which also enables lazy module loading and synchronous layout reads. The catch is that a synchronous call runs on the JS thread, so slow native work should still return a promise.

code

typescript · 11 lines
typescript
// Legacy bridge style (removed architecture): the result arrives in a callback.
// nativeModule.getValue((value) => nativeModule.doSomething(value));

// JSI-backed Turbo Native Module: a synchronous call with a return value.
declare const nativeModule: {
  getValue(): number;
  doSomething(value: number): void;
};

const value = nativeModule.getValue();
nativeModule.doSomething(value);

go deeper

for a junior

Remember the contrast: the bridge sent serialized asynchronous messages; JSI lets JavaScript call native code directly and synchronously.

for a middle

Explain host functions and host objects, why skipping serialization and holding references matters, and which parts of the New Architecture sit on JSI.

for a senior

Discuss the costs: synchronous calls block the JS thread, the runtime is single-threaded, and callbacks from native threads must be scheduled onto the JS thread.

for a principal

Frame JSI as the enabler for synchronous rendering and lazy modules, and judge which library capabilities justify synchronous native APIs versus asynchronous ones.

## Two ways across the language boundary A React Native app runs JavaScript in an engine (Hermes) and native code in C++, Kotlin, Java, Objective-C or Swift. Every call between them needs a mechanism. React Native has had two. The **legacy bridge** (the old architecture) treated the boundary like a message channel. JavaScript recorded each native call (module id, method id, arguments) in a queue, the queue was converted into JSON-compatible data, and native code processed it later on its own threads. Answers came back the same way, as another message that invoked a callback or resolved a promise. (A rarely used blocking-synchronous method type skipped the queue, at the cost of stalling JavaScript while native code ran.) **JSI**, the JavaScript Interface, is a C++ API that lets native code work with the engine's values directly. Its core type, `jsi::Runtime`, represents the running JavaScript engine; C++ code can read and create JS values, call JS functions and, most importantly, **expose C++ to JavaScript**: - a **host function**, created with `jsi::Function::createFromHostFunction`, is a C++ lambda that JavaScript calls like any function; - a **host object**, a subclass of `jsi::HostObject` passed to `jsi::Object::createFromHostObject`, is a C++ object whose property reads (`get`) and writes (`set`) are answered by C++ code. ## What actually changed | Aspect | Legacy bridge | JSI | |---|---|---| | Call style | Queued and asynchronous; results through callbacks or promises | Synchronous calls with a return value; async still possible | | Data | Arguments serialized into JSON-compatible values | JS values passed through; no serialization step required | | Native objects | Only copies of data could cross; JS could not hold a native object | JS can hold a reference to a C++ object and call its methods | | Batching | Calls queued and flushed in batches | Each call goes straight to C++ | | Startup | Modules registered and initialized when the bridge started | Turbo Native Modules load lazily, on first use | JSI itself is older than the New Architecture: late versions of the bridge already ran on top of it, with the message queue layered over the JSI runtime. What the New Architecture changed is that modules and the renderer use JSI directly instead of going through the queue. The New Architecture is built on this layer. **Turbo Native Modules** use JSI to expose typed native methods, and the **Fabric** renderer uses it to let React create shadow nodes in C++ synchronously, which is what makes synchronous layout reads and interruptible, prioritized rendering possible. ## Why the swap mattered 1. **Latency**: an ordinary, queued bridge call could not return a value, so reading one number from native code cost at least one round trip through the queue. With JSI, `const v = nativeModule.getValue()` returns immediately. 2. **Throughput**: serializing every argument became a bottleneck for frequent or large payloads, such as scroll events or big arrays. JSI passes values and references without that step. 3. **Consistency**: because the two sides only exchanged asynchronous messages, JavaScript and native state could drift apart and could not be reconciled synchronously, which showed up as blank list frames and visual jumps. 4. **Startup**: no bridge to initialize and no eager module setup; globals such as `setTimeout` can be bound from C++ with `runtime.global().setProperty(...)`. ## What JSI does not change - **It does not make JavaScript multi-threaded.** A `jsi::Runtime` must not be used from several threads concurrently; C++ code that wants to call back into JavaScript from another thread schedules the work onto the JS thread. - **Synchronous is not free.** A synchronous host function runs on the JS thread; a slow one blocks JavaScript for its whole duration. Long native work should still return a `Promise`. - **Most app developers never write raw JSI.** Libraries and Turbo Native Modules use it; app code calls typed module methods generated from a spec. ## In an interview Say the three words that carry the answer: **synchronous**, **no serialization**, **references**. Then give the cost: synchronous calls run on the JS thread, so they must be fast. That is the difference between knowing the slogan and understanding the mechanism. In React Native 0.87 there is no choice to make: the New Architecture has been the only architecture since 0.82, so every app already runs on JSI.

  • If JSI calls in React Native are synchronous, when should a native module method still return a Promise?
    Whenever the work is slow or waits on something: disk or network I/O, a permission prompt, heavy computation. A synchronous host function runs on the JS thread and blocks JavaScript until it returns, so it suits fast reads such as a cached value or a small calculation. Returning a Promise lets the native side do the work elsewhere and resolve later.
  • In React Native, why could the legacy bridge not pass a native object to JavaScript?
    Everything crossing the bridge was converted into JSON-compatible data: numbers, strings, booleans, arrays and plain objects. A native object has no such representation, so only a copy of some of its fields, or an id pointing into a native-side registry, could cross. JSI can hand JavaScript a host object whose property reads call straight into the living C++ object.

saying these in an interview costs you the question

  • JSI is just a faster serializer for the same bridge messages.
  • With JSI, JavaScript runs in parallel on the native threads.
  • Every JSI call is synchronous, so promises are no longer needed.
  • JSI is a Hermes-only feature that other engines cannot implement.
  • App developers must write raw JSI code to use the New Architecture.
open as a page

In a React Native 0.87 app, does JavaScript still talk to native code through the bridge, and what replaced it?

level: juniorimportance: should knowfreq 50%

basics

~10 s

No. React Native 0.87 runs bridgeless: JavaScript calls Turbo Native Modules and drives the Fabric renderer through JSI, with no serialized message queue. Libraries still written for the bridge work through interop layers.

open as a page

A React Native barcode-scanner library moves from bridge events to a JSI HostObject: what changes, and what threading rule must its C++ code obey?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Scans stop being serialized, queued bridge events: JavaScript holds a C++ host object and reads its state synchronously. The rule: jsi::Runtime must not be used concurrently, so the camera thread schedules work onto the JS thread instead of touching the runtime.

open as a page

How did React Native's legacy bridge batch JavaScript-to-native calls in its MessageQueue, and why did the arguments have to be serializable?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

MessageQueue recorded each call as a module id, method id and arguments, and handed the batch to native code on the next native-to-JS return or after 5 ms. Arguments crossed as JSON-compatible data, so functions, NaN and native objects could not.

open as a page