The execution model: the JS thread, the native UI thread, and the bridge
JavaScript's own event-loop lesson taught a single-threaded execution model as a given — one thread, one call stack, period. React Native genuinely has two real, separate threads running simultaneously, and understanding how they actually talk to each other explains a real, distinctive category of mobile performance bug.
Intermediate
3 min read
Two threads, not one
JavaScript's own event-loop lesson established a single-threaded execution model as fundamental to the language — and that's still true within the JS thread itself. React Native adds a second, genuinely separate thread: the native UI thread, which actually performs layout, drawing, and touch handling, running independently and in parallel with the JS thread. Your React components' code runs entirely on the JS thread; what actually appears on screen is the native thread's job.
The old bridge: asynchronous, serialized messages between the two threads
JS thread: "set this view's background color to red"
→ serialized into a JSON-like message
→ sent ACROSS the bridge, asynchronously
→ native thread: receives it, deserializes it, applies the change
The original React Native architecture (the bridge) communicates between the two threads entirely through asynchronous, serialized messages — every single interaction (a state update causing a re-render, a native event like a touch being reported back to JS) has to be packaged up, sent across, and unpacked on the other side. This works, but it's a real source of the specific performance problems React Native developers have historically had to design around: the bridge has real throughput limits, and every round trip carries real serialization overhead.
JSI and the New Architecture: direct calls, no serialization
The JSI (JavaScript Interface), part of React Native's "New Architecture," replaces the bridge's message-passing with direct, synchronous function calls between JS and native code — JS code can hold an actual reference to a native object and call methods on it directly, the same way it'd call a plain JavaScript object's method, with no serialization step and no inherent asynchronous round trip. This is a genuinely different mechanism, not just an optimized version of the same idea — it removes an entire category of overhead the bridge architecture couldn't avoid by design.
Why this distinction actually matters for writing real React Native code
Understanding that a native module call historically meant "send a message, wait asynchronously" explains a real, historically common pattern: older React Native code and libraries are full of promises and callbacks specifically for interactions with native code that, conceptually, should feel synchronous (reading a value that's already available natively) — a direct consequence of the bridge's async-only nature, not a deliberate API design choice. JSI-based native modules can expose genuinely synchronous APIs instead, closing this gap directly — recognizing which architecture a given library or project targets explains why its native-interaction code looks the way it does.