In a React Native project, I'm wondering how to efficiently manage the bridge between JavaScript code and native modules to reduce latency and avoid UI freezes. What strategies or patterns do you recommend to minimize frequent calls, optimize data transfer, and ensure good responsiveness on both sides of the bridge? Your feedback and experiences are welcome.
How to optimize communication between JavaScript and native in React Native?
👁️ 139 views💬 2 replies❤️ 0 likes
2 Replies
In my recent projects, I’ve noticed that the biggest gains in responsiveness come from reducing the number of bridge calls and better structuring the data being passed. Personally, I started by batching frequent calls using `BatchedBridge`, or if the project supports TurboModules, I migrated critical modules (e.g., geolocation or sensor reading) to TurboModules with JSI. This allows for a single "burst" of communication instead of multiple round-trips, which significantly cuts down latency. In practice, I keep payloads as lightweight as possible—only passing primitives (strings, numbers) and serializing complex objects to JSON only when absolutely necessary. For large data streams (e.g., images or result lists), I use the "native cache" pattern: the native side reads the file once, stores it in a `SparseArray`, and JavaScript only retrieves references (IDs) via a single call, then requests elements as needed.
Next, I shift any heavy logic to the native side (filtering calculations, video decoding, encryption) and expose them as async methods in the native module so the JavaScript thread stays free for the UI. For animations, I prefer `react-native-reanimated` or `useNativeDriver` in `Animated` to ensure rendering happens entirely on the native thread. Finally, I’ve found that using the Hermes engine and disabling remote debugging reduces GC pauses, preventing UI freezes during interactions. By combining batching, TurboModules with JSI, and logical separation, you get a much smoother and more responsive bridge.
One of the most critical steps to prevent UI stuttering is reducing the number of calls over the bridge. I’ve used a tactic that works well for me: **TurboModules + JSI**.
With JSI, we can directly bind native functions to the JavaScript side, almost eliminating async-await and synchronous blocking. Seriously, with this setup, we can process a data stream in milliseconds—you immediately notice the difference, especially with frequently updated sensor data or animation-heavy code.
Another trick is to **serialize data in-memory** (e.g., using Protobuf or MessagePack) and send it as a single package. This prevents the bridge from being called "too often." Instead of sending small objects, batching payloads also reduces message size. Additionally, using an **event-bus** approach—defining an event emitter on the native side and subscribing to it in JS only when needed—helps. For operations running in the background without blocking the UI thread, using `InteractionManager.runAfterInteractions` smooths out transitions.
Since React Native 0.71+, the full support for **Hermes** and **Fabric** is another big plus. Hermes’ fast garbage collection and JSI integration significantly reduce bridge latency. Finally, when working with large lists or tabular data, optimizing `FlatList`/`SectionList` with **batch rendering** and `windowSize` settings keeps data flow sustainable without overloading the UI thread.
Guys, I’d love to hear which combination worked best for you when you tried these approaches. If you’re working with JSI-based native modules, share your profiling results and any bottlenecks you’ve encountered—maybe we can figure out a clearer pattern together.