Day 31: RN Old Architecture: Bridge, Serialization & Async Bottlenecks
Understand exactly why the classic React Native Bridge causes jank — the async, JSON-serialized, batched messaging model — before comparing it to JSI.
Study
Concepts
Three threads, one async messaging Bridge
Classic React Native runs (at least) three threads: the JS thread (your React code, running in JavaScriptCore), the Native/UI (Shadow) thread that computes layout via Yoga and talks to native views, and the Main/UI thread that actually owns iOS/Android views. The Bridge is the ONLY channel between the JS thread and the native side — every call across it (invoking a native module, updating a view's props) is serialized to JSON, queued, and sent asynchronously in batches, then deserialized on the other side.
This design was intentional: it fully decouples JS from native (no shared memory, easier to reason about, easier to run JS off-thread), but it means EVERY cross-boundary interaction pays a serialization + queueing + async round-trip cost — fine for occasional calls, a real bottleneck for anything high-frequency.
Where the bottleneck actually shows up
Gesture-driven UI (drag, swipe, scroll-linked animations) needs to update native view properties on every frame (~16.6ms budget at 60fps). Routing that through JSON serialization + an async Bridge round-trip per frame is where classic RN visibly drops frames — this is precisely the problem `react-native-reanimated`'s worklet model and `react-native-gesture-handler` were built to route around, by running logic closer to the UI thread instead of bouncing through the Bridge per frame.
Because the Bridge is asynchronous by construction, calling a native method never returns a value directly — it always goes through a callback/Promise, which is fine for network-style calls (fetch a file, read from storage) but forces awkward patterns for anything that conceptually wants a synchronous answer (e.g. "is this native view currently visible?").
See It
Visualizations
Visualization
Old Architecture: three threads talking through the Bridge
Build It
Code Examples
Every classic native module call is inherently async
import { NativeModules } from 'react-native';
const { DeviceInfo } = NativeModules;
// Cannot get a value back synchronously — must cross the Bridge
// asynchronously even for data that is already available natively.
async function readBatteryLevel() {
const level = await DeviceInfo.getBatteryLevel(); // serialize → queue → deserialize
console.log('battery:', level);
}
// A gesture-driven animation naively calling setNativeProps on every
// touch move pays this round-trip cost on every single frame:
function onTouchMove(event) {
viewRef.current.setNativeProps({
style: { transform: [{ translateX: event.nativeEvent.pageX }] },
}); // -> JSON-serialized, queued, sent over the Bridge, every frame
}At 60fps you have ~16.6ms per frame for JS logic, layout, AND this round trip — any Bridge congestion (large batched payloads, other pending calls) can push you past that budget and drop frames.
Remember
Key Takeaways
- Classic RN has 3 threads (JS, Shadow/layout, Native UI) connected only by the asynchronous, JSON-serializing Bridge.
- Every cross-boundary call pays a serialize → queue → async-deliver → deserialize cost, batched for efficiency but never free.
- High-frequency interactions (gestures, per-frame animation) are where this cost becomes visibly janky.
- The Bridge is asynchronous by design — even conceptually "instant" native reads must go through a callback/Promise.
- This exact bottleneck is the direct motivation for the New Architecture (JSI, Day 32) — know the "why" before the "what".
Do It
Practice
- 1Read the RN docs page on the legacy Bridge and diagram the object/message shape of one batched call by hand.
- 2Profile a naive setNativeProps-per-touch-move animation with the RN Perf Monitor and note dropped frames under load.
- 3List three real product features (from apps you use) that would visibly suffer from Bridge-round-trip-per-frame costs.