Over the past few years, I’ve worked with a few React Native projects. A mix of starting them from scratch or trying to rescue them after becoming sluggish and I have to admit that React Native is a great piece of technology. You have this framework that allows you to write familiar React code without worrying about how you will translate that into UI on Android, iOS, desktop and Web. It is efficient at figuring out what needs to be rendered and rendering it using native platform APIs, and that’s its real power compared to Flutter and Compose Multiplatform, where both render like a game engine.
Like anything in life, convenience comes with compromises. The biggest compromise I see React Native applications make is staying within the single-threaded confinement for everything the application does. React Native makes it easy for you to fall into this trap because you are pushed to write JavaScript starting from your UI logic.
A typical application you might see has React and React Query. It makes an API call, maps the response into something the UI can render. In this flow, the only thing that runs on a background thread is the network request. All the other logic is competing for execution time with UI rendering. Initially, this issue is harmless since you start with very little data mapping or non-UI logic. The more your application grows, the more non-UI logic you have on that single thread, and the slower your application becomes—and no, using JSI and C++ implementation is not the answer because it still runs on that same JavaScript thread. It’s not React Native’s fault, but it definitely created an environment where falling into this trap is so easy.
So how do you not fall into this trap?
The answer is to write your own native modules. This forces you to keep the React code purely UI-only and allows you to leverage multi-threading for all your other logic. In the previous example, writing your own native module to fetch, cache and map the data to a UI state can happen on a background thread, then rendering happens on the JavaScript thread.
You still have the cost of “bridge communication,” but that is getting phased out with latest React Native releases. Your main challenge would be learning the native platforms you are running on.
At Treecard, our Wildhero app is React Native-based, and we got into this trap and this conclusion. Our fix was to move the app’s non-UI logic to native. We started using Expo Modules API to scaffold native modules around native code that fetches, caches and maps the data.
Our native code is mostly written with Kotlin Multiplatform which gets compiled to native code for both iOS and Android. We use Expo’s API to create React Native modules that acts as an adapter between React Native world and the Kotlin logic.
I love using cross-platform frameworks, and I see the potential and usefulness of them. I also am pragmatic and know that all abstractions come at a cost. Sometimes this cost can be accepted, and other times you have to avoid it.