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 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 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
mapinstead of a virtualised list, which renders rows nobody will see. - Hand-edited
iosandandroiddirectories, 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.