DEV Community

Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

Scaling React Native Performance: When to Move Logic to C++ via JSI

In the lifecycle of a React Native application, you eventually hit a wall that no amount of memoization or FlatList optimization can fix. You see it in the profiler: the JavaScript thread is pegged at 100%, frame rates are dropping into the teens, and the bridge is congested with serialized JSON.

During my eight years of professional software engineering, I have shipped 18 production applications across iOS, Android, web, and desktop. I have seen this bottleneck manifest in various forms, from real-time data visualization to complex state synchronizations. Most recently, as the founding engineer at Synapsis Medical Technologies, I owned the React Native and Next.js architecture from 0 to 1. When building a HealthTech AI platform that integrates wearables and RAG-based clinical AI pipelines, the cost of a sluggish UI isn't just a poor user experience—it’s a barrier to clinical utility.

When the JS thread becomes the bottleneck, the standard advice is to "move it to the native side." But the traditional asynchronous bridge is often the very thing causing the lag. This is where the JavaScript Interface (JSI) and C++ HostObjects become necessary.

The Bottleneck: Why the Bridge Fails

The traditional React Native bridge operates on a message-passing architecture. To move data from the JS environment to C++, Java, or Objective-C, the data must be serialized into JSON, sent across the bridge, and deserialized on the other side. This is inherently asynchronous and introduces significant overhead, especially for high-frequency updates like sensor data or real-time audio processing.

At Synapsis, I led the architecture for systems handling FHIR/HL7 data and wearable integrations. When you are processing high-frequency biometric streams, the millisecond delays introduced by bridge serialization accumulate. If your JS thread is busy calculating complex transformations on large datasets, the UI becomes unresponsive because the same thread handles user interactions and layout triggers.

JSI changes this by allowing JavaScript to hold a direct reference to C++ objects. There is no serialization. You are calling a C++ function directly from the JS engine (Hermes or V8) as if it were a native JS function.

Architecture: HostObjects and Memory Ownership

The core of a high-performance JSI module is the jsi::HostObject. A HostObject is a C++ class that you can expose to the JavaScript runtime. Unlike a standard bridge module, a HostObject lives in the C++ memory space, and the JS garbage collector (GC) manages a reference to it.

When designing these modules, the primary trade-off is memory ownership. If you allocate memory in C++ that the JS thread needs to access, you must decide who owns the lifecycle of that data.

  1. Shared Ownership: Using std::shared_ptr to manage the C++ object so it persists as long as either the C++ side or the JS side holds a reference.
  2. JS-Managed: The object is created and destroyed based on the JS GC cycle.
  3. Manual Lifecycle: For high-performance buffers (like image data or raw byte arrays), you might manage the memory manually to avoid GC pauses, but this introduces the risk of memory leaks if the JS reference is dropped without a proper cleanup call.

In my experience building HIPAA-aligned RAG and LLM pipelines, maintaining 99.9% uptime required rigorous memory management. When moving logic to C++, you are stepping outside the safety net of the JS GC. You gain raw speed, but you inherit the responsibility of preventing memory fragmentation and leaks that can crash an app after hours of use.

Measuring Before You Rewrite

Before writing a single line of C++, you must prove that the JS thread is actually the problem. I have seen teams spend weeks porting logic to C++ only to find that their performance issues were caused by unnecessary re-renders in the React layer or inefficient SQL queries in the local database.

Use the Flashlight tool or the built-in React Native profiler. Look for:

  • JS Frame Rate: If this is low while the UI thread is high, your JS logic is too heavy.
  • Bridge Traffic: Use a bridge monitor to see the volume of messages. If you see megabytes of JSON crossing the bridge every second, JSI is the solution.

At Synapsis, I oversaw a CI/CD overhaul that cut release cycles from 2 days to 4 hours across 5 production systems. This efficiency was only possible because we prioritized data-driven decisions over architectural hunks. We only moved to C++ when the profiler showed that the overhead of serializing wearable data packets was exceeding our 16ms frame budget.

A Worked Example: The JSI HostObject

To implement a JSI module, you define a class that inherits from jsi::HostObject. You then override the get and set methods. These methods act as traps for property access from JavaScript.

#include <jsi/jsi.h>

using namespace facebook;

class DataProcessorHostObject : public jsi::HostObject {
public:
  // This method is called when JS accesses a property on the object
  jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override {
    auto propertyName = name.utf8(runtime);

    if (propertyName == "processData") {
      return jsi::Function::createFromHostFunction(
          runtime,
          name,
          1, // Number of arguments
          [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value {
            if (!arguments[0].isObject()) {
              return jsi::Value::undefined();
            }

            // Direct access to the underlying data without JSON serialization
            jsi::Object input = arguments[0].getObject(runtime);
            // Perform high-performance C++ logic here

            return jsi::Value(42); // Return result directly to JS
          });
    }

    return jsi::Value::undefined();
  }
};
Enter fullscreen mode Exit fullscreen mode

To expose this to your React Native app, you need to install the bindings during the app initialization phase. This is typically done in the TurboModule setup or a custom JSIInstaller.

The Cost of Learning C++ JSI

The learning curve for JSI is steep. Unlike the standard bridge, which uses familiar Java/Objective-C patterns, JSI requires an understanding of:

  • The JSI Value System: Understanding how jsi::Value, jsi::Object, and jsi::String map to JS types.
  • Thread Safety: JSI is not thread-safe by default. If you are doing heavy computation on a background C++ thread, you cannot directly touch the jsi::Runtime from that thread. You must dispatch back to the JS thread to return results, or use a jsi::Runtime instance dedicated to that thread if you are running a multi-threaded JS environment (which is rare in standard RN).
  • Build System Complexity: Integrating C++ via CMake or ndk-build into an existing React Native project adds significant complexity to your CI/CD pipeline.

During my time scaling the engineering team from 0 to 21 engineers at Synapsis, one of the biggest challenges was ensuring that the team could maintain the C++ layer. We implemented strict code review standards and heavy unit testing for the C++ modules because a crash in C++ is a hard crash for the entire app—no RedBox, no stack trace in Logcat that points to a JS line number.

Practical Recommendations

If you find yourself needing to move logic to C++, follow these guidelines:

  1. Keep the C++ Layer Thin: Only move the "hot path"—the specific loops or transformations that are slow. Keep the business logic in JavaScript where it is easier to iterate on and test.
  2. Use TypedArrays: If you are passing large amounts of numerical data (like sensor readings), use Int32Array or Float64Array. JSI can access the underlying buffer of a TypedArray with almost zero overhead.
  3. Minimize Context Switching: The transition from JS to C++ is fast, but it is not free. It is better to make one JSI call that processes 1,000 data points than 1,000 JSI calls that process one data point each.
  4. Automate the Bindings: For larger projects, consider using tools like rn-gen or custom code generators to create the JSI boilerplate. Manual JSI code is error-prone and tedious to maintain.

Conclusion

JSI is the most powerful tool in the React Native performance arsenal, but it is a tool of last resort. In my work as an independent Systems Architect and formerly as a founding engineer, I have found that the most successful implementations are those that respect the boundary between high-level JS and low-level C++.

By moving the heavy lifting to C++ HostObjects, you free up the JS thread to do what it does best: manage the UI and handle user interaction. The result is an application that feels truly native, capable of handling the complex, real-time demands of modern HealthTech and AI-driven platforms without compromising on the development velocity that React Native provides.


Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: www.amitchakraborty.dev · LinkedIn · GitHub. Open to senior and founding engineering roles, remote worldwide.

Top comments (0)