/blog
Engineering

React Native or native: what actually decides it

It depends, so here are the dependencies. Platform surface area, team shape, data frequency and release cadence, and how each one should move the decision.

Soccotra

Most writing on this question argues a side. That is the wrong shape for the problem: the same app, built by two different teams with two different feature sets, can correctly land on two different answers.

What follows is not a verdict. It is the set of variables that decide it, in roughly the order they should be weighted.

The deciding variable is platform surface area

Before anything else, work out what fraction of the app touches APIs that only exist on the platform, and how deep that contact goes.

Screens that render data from an HTTP API are the same work in every stack. The divergence starts at the operating system boundary: camera capture pipelines with per-frame processing, background location, Bluetooth Low Energy, home-screen widgets, share extensions, Siri and App Intents, HealthKit, CarPlay and Android Auto, watch and wear apps.

React Native has community wrappers for much of that list, and the good ones are genuinely good. But wrappers are thinnest exactly where the OS is most awkward: background execution, permission state machines, restoration after the system kills your process. CoreBluetooth state restoration, the difference between significant-change and continuous location, Android foreground services and vendor battery optimisation are all behaviours you have to reason about directly. A wrapper does not remove that reasoning; it adds a layer between you and it.

Some surfaces have no wrapper at all, by construction. WidgetKit widgets and Live Activities run as separate SwiftUI processes. There is no JavaScript runtime inside them. The same is true of watchOS apps. If widgets are a headline feature rather than a nicety, you are writing Swift no matter what you choose for the main app.

So the first question is not "which is faster to build" but: is platform-specific work at the edge of this product, or is it the core loop?

Team shape is the second variable, and it is undersold

For a two-engineer team the practical question is not lines of code. It is review and continuity.

One codebase means both engineers can read every change. Two codebases usually means each person becomes the sole owner of one, and sole ownership has predictable costs: no informed second reader on pull requests, no cover when someone is ill or on holiday, and slow behavioural drift between the platforms that turns QA into a matrix instead of a checklist.

The counter-argument is that React Native only defers native knowledge. Someone still owns signing, provisioning, Gradle and CocoaPods upgrades, and the periodic version bump that touches both native projects at once. Budget for that rather than assuming cross-platform means never opening Xcode.

The bridge cost is narrower than it used to be, but it still has a shape

The old React Native architecture serialised every call between JavaScript and native into batched asynchronous messages. At 60 Hz you have about 16.7 ms per frame, and on a 120 Hz display about 8.3 ms. A gesture that had to make a round trip through JavaScript to decide where a view goes would sometimes miss that budget, which is where the reputation for janky animation came from.

The New Architecture changes the mechanics. JSI lets JavaScript hold direct references to native objects, TurboModules load lazily and can be called synchronously, and Fabric renders through a shared C++ core. Reanimated runs animation and gesture worklets on the UI thread, so a drag never waits on JavaScript.

What has not changed is the cost of crossing the boundary often. High-frequency data is the failure case that survives: BLE characteristic notifications arriving at tens of hertz, accelerometer streams, live telemetry from a connected device. Emitting each packet as a JavaScript event and re-rendering from it will struggle regardless of architecture.

The fix is architectural, not a matter of framework choice. Keep the hot loop native and cross the boundary at the rate the interface needs: aggregate, decimate, emit settled state rather than raw packets. A chart redrawing at 60 fps only needs a windowed buffer, not 200 individually delivered samples a second.

That gives a useful test. If you can describe the reduction that makes the data rate reasonable, React Native is fine. If the product is the raw stream and every sample matters visually, the core loop wants to be native.

Release cadence pulls in the opposite direction

Native means every change is a binary, and every binary goes through review. Apple reports that most submissions are reviewed in under 24 hours, so review is rarely the bottleneck people fear. But a typo fix costs the same as a feature: build, submit, wait, then a phased rollout on top.

React Native lets you update the JavaScript bundle over the air within store rules, which permit code updates that do not materially change the app's purpose. A copy correction becomes minutes instead of days. Two caveats: it only covers JavaScript, so any native module change is still a binary, and you now have a code path reaching users without review, which needs the staged rollout and rollback discipline you would apply to a backend deploy.

Native has the mirror advantage. New OS capabilities are available in Swift and Kotlin the day they ship, while React Native waits for a wrapper. If being early on platform features is part of the pitch, that lag is a recurring tax.

Hybrid: when native modules inside React Native are right

The good version has a small number of wide, infrequent boundaries. One native module owns the whole BLE session: scanning, connection, the reconnect state machine, parsing, and exposes a handful of events describing device state. The camera pipeline is native; everything wrapped around it is JavaScript. The contract is coarse and stable.

The bad version spreads native code thinly across many features. Every change then needs three implementations and a contract between them, and the boundary itself becomes where bugs live. A reliable signal that you are in this state: pull requests routinely touch JavaScript, Swift and Kotlin together. At that point you have three codebases plus glue, paying the coordination cost of native with the debugging ergonomics of cross-platform.

A short way to run the decision

List every screen and background behaviour. Sort each into three buckets: pure UI over an API; needs a platform capability with a mature wrapper; needs a platform capability you would write yourself.

If the third bucket is the core loop, build native. If it is one or two features at the edge, build React Native with deliberate native modules around them. If nearly everything is in the first bucket, choose React Native and stop deliberating. The decision is not load-bearing.

One last consideration: pick the direction that is cheaper to be wrong about. React Native to native is a rewrite of the app, but the API contract, data model and product decisions survive intact. That is a smaller loss than discovering late that two people cannot keep two codebases in step.


We work in both stacks, and we would rather talk you out of the wrong one early. See mobile apps.