DevelopmentAll levels

React Native

React Native is React, rendering real platform views instead of DOM nodes. It is not a web view and it is not a compiler to Swift or Kotlin: your JavaScript runs in its own engine and tells the platform what to put on screen. That architecture is why a web team can ship a real mobile app, and it is also the source of every performance question the framework has. This page covers the threading model, Expo, native modules, and where the illusion breaks.

Views
Real platform views
Your code
One JS thread
Shared
Logic, not the whole UI

What is actually on screen

A <View> in React Native is not a div and not a drawing. It becomes a real UIView on iOS and a real android.view.View on Android, created by the platform and drawn by the platform. That is why scrolling has the right physics, why the keyboard behaves correctly, and why the app inherits accessibility for free — none of that is reimplemented, it is the operating system doing its own job.

Your React code runs on one JavaScript thread. The views the user actually touches are real native views on the platform's main thread, and layout is measured on a third. Everything either side needs from the other has to cross between them — the old bridge serialised those messages and batched them, the new architecture calls across directly through JSI. Anything driven from the JavaScript side stops when that thread is busy, which is why a gesture animated natively stays smooth while one animated from JavaScript stutters.

Your JavaScript, meanwhile, runs in a separate engine — Hermes by default now — on its own thread. It never touches those views directly. It describes what the tree should look like, that description crosses to the native side, a third thread measures the layout, and the main thread applies the result. Reading a diagram of it once is worth more than any list of optimisation tips, because every performance problem in a React Native app is a question about that crossing.

The original design serialised every message across that boundary as JSON and sent it in batches — the "bridge". It worked, and it produced the framework's signature failure: a busy JavaScript thread means the native side gets its instructions late, and frames are dropped. The new architecture replaces it. JSI lets JavaScript hold a direct reference to a native object and call it synchronously, Fabric rebuilds rendering on top of that, and TurboModules load native modules lazily instead of all at startup.

Expo, and why you should start there

Expo used to be a sandbox you eventually had to leave. It is not that any more, and the advice has genuinely changed: start a new React Native project with Expo unless you have a specific reason not to. It gives you a managed build service, over-the-air updates, a large set of maintained native modules, and — most importantly — config plugins, which describe native project changes as code instead of as edits you make by hand and lose on the next upgrade.

That last point is the one that decides how a project ages. A bare React Native app contains an ios and an android directory that you edit directly, and every upgrade of the framework becomes a merge against files you have changed. With config plugins, those directories are generated, so an upgrade regenerates them and your customisations are reapplied. Teams that hand-edit native projects are the ones still stuck three major versions behind two years later.

How much you actually share

The promise people hear is one codebase for two platforms. The honest number is that business logic, networking, state and most screens are shared, and the remainder is not. Navigation patterns differ, the back gesture exists on one platform and not the other, permissions dialogs behave differently, and every store has its own rules about what you must show. React Native gives you Platform.select and .ios.tsx / .android.tsx files precisely because the last ten per cent is expected to diverge.

ConcernShared?What that means in practice
Business logic, API, stateYes, entirelyThe bulk of what you write, and the reason the approach pays off at all.
Screens and componentsMostlySame code, with platform branches for spacing, headers and back behaviour.
Navigation feelPartlyThe library abstracts it; users still expect their platform's conventions.
Permissions, push, deep linksNoConfigured per platform, and each store reviews them on its own terms.
Release and reviewNoTwo pipelines, two sets of certificates, two review queues, forever.

When a capability has no JavaScript API, you write a native module: a small piece of Swift or Kotlin exposed to your JavaScript. This is normal and not a failure of the approach, but it is the moment the team needs someone who can read the platform documentation. A React Native project with no one who understands the native side will eventually stall on a problem no amount of JavaScript can reach — a background upload, a widget, a payment SDK that only ships native.

How this shows up in real delivery

The recurring performance complaint is lists. A long list rendered with map builds every row whether or not it is on screen, and on a mid-range Android phone that is the difference between an app that feels native and one that does not. Use a virtualised list, give it a stable keyExtractor, memoise the row component, and keep row heights predictable. Almost every "React Native is slow" report resolves to a list, an unnecessary re-render, or an image that was never resized.

The other thing to plan for early is upgrades. React Native moves quickly and the ecosystem around it moves with it, so a project that skips versions for a year faces an upgrade where the framework, the navigation library, the native modules and the platform SDKs all move at once. Upgrading one minor version at a time is dull and takes an afternoon; upgrading four at a time takes a fortnight and breaks things nobody can attribute.

Where it degrades

  • Animations driven from JavaScript, which stutter the moment the thread has real work to do.
  • Long lists built with map instead of a virtualised list, which renders rows nobody will see.
  • Hand-edited ios and android directories, which turn every framework upgrade into a merge conflict.
  • One layout shipped to both platforms with no thought for their navigation conventions.
  • A team with no native skill at all, which stalls the first time a feature needs a module.
  • Testing only on a recent iPhone, where the JS thread is fast enough to hide every mistake above.
  • Relying on over-the-air updates for changes that touch native code, which cannot ship that way.

When to use it

Use it when

  • A product that needs both platforms and whose team already writes React and TypeScript.
  • Content, commerce and workflow apps — forms, lists, navigation, network — which is most of what gets built.
  • Products that benefit from shipping fixes without a review queue, within the limits of what OTA can carry.
  • Teams that want native views and native accessibility rather than a rendering of them.

Avoid it when

  • Graphics-heavy apps, games and anything with continuous custom animation — that fights the architecture.
  • Apps built around a platform capability that has no JavaScript surface and changes with every OS release.
  • A single-platform product, where the abstraction costs you and buys nothing back.
  • Teams with no capacity to maintain two release pipelines — the crossplatform part is the code, not the shipping.

Found this useful?

Share it with someone who is working on the same problem.