JavaScript Interface
If you’ve ever rage-closed a Metro bundler tab after your animations stuttered like a dial-up modem, you’ve probably been haunted by one word: performance. And in React Native, that word has a history.
JSI is the unsung hero (or misunderstood antihero) of React Native’s New Architecture. It’s not a tool you install. It’s not an API you reach for. It’s the plumbing. The switchboard. The layer beneath the layers that makes synchronous communication between JavaScript and native code not just possible—but fast.
React Native’s original design (circa 2015) relied on what’s often called “the bridge.” Think of it like a postal service between JavaScript and the native world. Whenever JavaScript wanted something from iOS or Android—opening the camera, checking a sensor, playing audio—it had to serialize the request into JSON, drop it into a queue, and ship it across. The native side would then pick it up, unpack it, and do the job.
This worked. Kind of. But the bridge was asynchronous and batch-based, which made some things feel sluggish or outright inflexible. You couldn’t call native code synchronously. You couldn’t share memory. And because of the async, multi-threaded nature, debugging often felt like chasing ghosts through a hall of mirrors.
UI code in JS was fast. But anything involving native modules—animations, gestures, camera feeds, crypto, file IO—felt just disconnected enough to remind you this wasn’t the web. It was powerful, yes, but brittle.
JSI—short for JavaScript Interface—was Meta’s answer to these pain points. Instead of messages on a bridge, it exposes a direct C++ API between JavaScript and native code. No queues. No serialization. No middlemen.
With JSI, native functionality is registered as C++ “host objects” inside the JS runtime. That means JavaScript can call native code directly, synchronously, like any other JS object. Under the hood, it’s all C++, but from JavaScript’s point of view, it might as well be a regular object.
Want to call a native function? You just… call it.
class MyModule : public jsi::HostObject {
public:
jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override {
if (name.utf8(rt) == "now") {
return jsi::Value(static_cast<double>(time(nullptr)));
}
return jsi::Value::undefined();
}
};
Suddenly, nativeModule.now() in JavaScript maps directly to a C++ function, with no delays, marshaling, or gymnastics.
JSI doesn’t just make things faster—it makes them possible. By abstracting the JavaScript engine itself, it gives React Native runtime flexibility: Hermes, V8, or JavaScriptCore can all slot in under the same interface. This means tools like Reanimated, Skia, and Unistyles can run near-native operations entirely from JavaScript—no bridge, no wait.
It’s also the backbone behind the New Architecture. TurboModules use it. Fabric relies on it. Libraries like Nitro generate type-safe C++ bindings from TypeScript because JSI gives them access to jsi::Runtime directly.
But don’t mistake it for a cozy abstraction. JSI is low-level. Manual memory management. No thread safety baked in. If something goes wrong, it doesn’t warn you—it segfaults.
Still, if your app feels smoother, loads faster, or stops janking when you scroll… JSI is probably doing its job. Quietly. Efficiently. Dangerously close to the metal.