How to Choose Tools Wisely and Build Fast-Working React Native Apps

React Native may allow you to create fast-working and easy-to-maintain apps. But to make that happen, you need to first find the balance between native and JavaScript. Otherwise, your app can slow down significantly because, in simple words, it will be overloaded. Since any harm to app performance equals damage to business, we decided to share with you some best practices on finding that harmony.

Find the balance between native and JavaScript to build fast-working and low-maintenance apps

Issue: While working on native modules, you draw the line in the wrong place between native and JavaScript abstractions

When working with React Native, you're going to be developing JavaScript most of the time. However, there are situations when you need to write a bit of native code. For example, you're working with a third-party SDK that doesn't have official React Native support yet. In that case, you need to create a native module that wraps the underlying native methods and exports them to the React Native realm.

All native methods need real-world arguments to work. ReactNative builds on top of an abstraction called a bridge, which provides bidirectional communication between JavaScript and native worlds.

Note: There's an ongoing effort to move away from asynchronous bridge communication to a synchronous one.

As a result, JavaScript can execute native APIs and pass the necessary context to receive the desired return value. The communication itself is asynchronous – it means that while the caller is waiting for the results to arrive from the native side, the JavaScript is still running and may already be up for another task.

communication between the bridge react native and JSON

The number of JavaScript calls that arrive over the bridge is not deterministic and can vary over time, depending on the number of interactions that you do within your application. Additionally, each call takes time, as the JavaScript arguments need to be stringified into JSON, which is the established format that can be understood by these two realms.

For example, when the bridge is busy processing the data, another call will have to block and wait. If that interaction was related to gestures and animations, it is very likely that you have a dropped frame – the operation wasn't performed causing jitters in the UI.

React Native frames

Certain libraries, such as Animated provide special workarounds. In this case, use NativeDriver, which serializes the animation, passes it once upfront to the native thread and doesn’t cross the bridge while the animation is running – preventing it from being subject to accidental frame drops while another kind of work is happening.

That’s why it's important to keep the bridge communication efficient and fast.

More traffic flowing over the bridge means less space for other things

Passing more traffic over the bridge means that there is less space for other important things that React Native may want to transfer at that time. As a result, your application may become unresponsive to gestures or other interactions while you're performing native calls.

If you are seeing a degraded UI performance while executing certain native calls over the bridge or seeing substantial CPU consumption, you should take a closer look at what you are doing with the external libraries. It is very likely that there is more being transferred than there should be.

Solution: Use the right amount of abstraction on the JS side – validate and check types ahead of time

When building a native module, it is tempting to proxy the call immediately to the native side and let it do the rest. However, there are cases (like invalid arguments) which end up causing an unnecessary round-trip over the bridge only to learn that we didn’t provide the correct set of arguments.

Let’s take a simple JavaScript module that does nothing more but proxies the call straight to the underlying native module.

import { NativeModules } from "react-native";
const { ToastExample } = NativeModules;
export const show = (message, duration) => {
ToastExample.show(message, duration);
};

In the case of an incorrect or missing parameter, the native module is likely to throw an exception. The current version of ReactNative doesn't provide an abstraction for ensuring the JavaScript parameters and the ones needed by your native code are in sync. Your call will be serialized to JSON, transferred to the native side, and executed.

That operation will perform without any issues, even though we haven't passed the complete list of arguments needed for it to work. The error will arrive when the native side processes the call and receives an exception from the native module.

In such a scenario, you have lost some time waiting for the exception that you could've checked for beforehand.

import { NativeModules } from "react-native";
const { ToastExample } = NativeModules;
export const show = (message, duration) => {
if (typeof message !== "string" || message.length > 100) {
throw new Error("Invalid Toast content");
}
if (!Number.isInteger(duration) || duration > 20000) {
throw new Error("Invalid Toast duration");
}
ToastExample.show(message, duration);
};

The above is not only tied to the native modules themselves. It is worth keeping in mind that every React Native primitive component has its native equivalent and component props are passed over the bridge every time there's a rendering happening – or is it? It's not always the case when a component re-renders. ReactNative renderer is smart enough to diff the parts of our JSReact component hierarchy and only send enough information through the bridge so that the native view hierarchy is updated.

This is the case when styling components like e.g. View or Text using the style prop. Let's take a look at the following example using inline styles.

import React from "react";
import { View } from "react-native";
const App = () => {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
backgroundColor: "coral",
width: 200,
height: 200,
}}
/>
</View>
);
};
export default App;

Even though the style prop is passed as an inline object, it doesn't cause us any performance issues. Neither when we dynamically change the styles based on props, nor when we re-render the App component. View passes its props almost directly to the underlying native representation. And thanks to the ReactNative renderer, no matter how often we re-render this component on the JS side, only the smallest amount of data necessary to update the style prop will be passed through the bridge.

In React Native we have nicer ways to deal with styling and it's through StyleSheet API – a dedicated abstraction similar to CSS StyleSheets. Although it provides no performance benefits, it's worth calling it out for the ease of development and maintenance. When we develop our app in TypeScript or Flow, StyleSheet is well typed and makes it possible for our code editors to auto-complete.

The codebase is faster and easier to maintain

Whether you're facing any performance challenges right now, it is smart to implement a set of best practices around native modules as the benefits are not just about the speed but also the user experience. Sure, keeping the right amount of traffic flowing over the bridge will eventually contribute to your application performing better and working smoothly.

As you can see, certain techniques mentioned here are already being actively used inside React Native to provide you with a satisfactory performance out of the box. Being aware of them will help you create applications that perform better under a heavy load.

One additional benefit that is worth pointing out is the maintenance.

Keeping the heavy and advanced abstractions, such as validation, on the JavaScript side will result in a very thin native layer that is nothing more but just a wrapper around an underlying native SDK. In other words, the native part of your module is going to look more like a copy-paste from the documentation – comprehensible and specific.

Mastering this approach to the development of native modules is why a lot of JavaScript developers can easily extend their applications with additional functionality without specializing in Objective-C or Java.

Need help with performance optimization?

We are the official Meta partners on React Native. We’ve been working on React Native projects for years, delivering high-quality solutions for our clients and contributing greatly to the React Native ecosystem. Our Open Source projects help thousands of developers cope with their challenges and make their work easier every day. Contact us if you need help with cross-platform or React Native development.

Insights

Learn more about Performance

cover

Jul 3Podcast

Why Nitro Is More Than a Faster Bridge

Marc Rousavy (Margelo) talks Nitro modules, native state, zero-copy data, Vision Camera, and why speed is only part of the story.

cover

May 25Article

Profile React Components in React Native Release Builds

Learn how to profile React components in React Native release builds with @callstack/inspector, using an Expo app setup that requires zero native code changes.

cover

Nov 21Article

Memory Ownership Models: When JavaScript Meets Native Code

A deep dive into how React Native handles memory when JavaScript and native code share ArrayBuffer data. Learn why async processing can break, how GC affects raw pointers, and the safe patterns for building high-performance native integrations.

cover

Oct 14Article

From Files to Buffers: Building Real-Time Audio Pipelines in React Native

Learn how to move beyond file-based audio APIs and work directly with in-memory PCM data in React Native. This guide shows how to construct WAV buffers, use JSI for zero-copy transfers, and enable real-time audio processing.

cover

Sep 8Article

How to Add Custom JSI Bindings in Your TurboModule (and Why You Might Need Them)

TurboModules provide a type-safe way to write native code in React Native. JSI sits underneath as a lower-level API, enabling direct interaction with the JavaScript runtime. Beyond what TurboModules expose, JSI gives you access to powerful features, such as working directly with ArrayBuffers. This article shows how to inject a custom binding step by step.

cover

Jul 24Article

Exploring Fibers: Cooperative Multitasking and Lock-Free Task Execution

Explore how fibers enable lightweight, cooperative multitasking. This guide walks through building a simple scheduler, managing task dependencies without locks, and using Tracy to visualize performance.

cover

May 21Article

Multithreading Isn’t Free: Performance Pitfalls Visualized

Learn how to profile and optimize multithreaded apps. Discover why more threads don’t always mean faster results and how bad thread contention really is.

Event Cover

Apr 28Webinar

Master JavaScript Performance in React Native (Part 2)

Learn to fix JS performance issues in React Native-from debounce bugs to Redux selectors, in this hands-on webinar with real code examples.

Events Cover

Apr 24Talk

Tips & Tricks From The Ultimate Guide to React Native Optimization 2025

View flattening, React 19 hooks, real DevTools tips-learn what actually helps your React Native app run faster.

Events Cover

Apr 24Talk

User-Focused Performance Tracking

Discover how Expensify defines and tracks performance metrics that actually impact user experience.

cover

Dec 18Podcast

React Native FastIO: Nitro-Powered Performance Boost

Listen to Mike Grabowski talk about React Native FastIO, a collection of primitives for working with data in React Native and its performance benefits.

cover

Oct 15Podcast

Superfast Nitro Modules With Marc Rousavy

Even faster React Native apps? That's possible with Nitro Modules! Tune in to the conversation with their creator, Marc Rousavy.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.