You can ship a React Native app without knowing any of this. You cannot debug a strange performance problem without it, and the vocabulary turns up in every release note and every interview.
The shape of it
Three things are running.
A JavaScript engine executes your code — your components, your state, your effects. It is Hermes, built by Meta for this job.
The native side owns the actual UI. Real UIViews on iOS, real
Views on Android, laid out and drawn by the platform.
A layout engine, Yoga, computes flexbox. It is written in C++ and shared by both platforms, which is why your layout behaves identically on each — and why the flexbox defaults differ from the web's: Yoga is not a browser and made its own choices.
React reconciles as it always does. What changes is the renderer: instead of producing DOM mutations it produces instructions for native views.
The old bridge, and why it was replaced
Originally the two sides communicated over an asynchronous bridge. Every call was serialised to JSON, queued, and delivered in a batch. JavaScript could not call a native function and get an answer — it sent a message and something happened later.
That design was the source of the era's characteristic problems. Serialising everything is expensive at volume, which is why a scroll handler driven from JavaScript stuttered. Nothing was synchronous, so measuring a view meant a round trip. And startup was slow because every native module was initialised whether you used it or not.
JSI
The JavaScript Interface replaced it, and it is the foundation of everything else. JSI is a lightweight C++ layer that lets JavaScript hold a reference to a C++ object and call its methods directly — synchronously, with no serialisation.
Two consequences worth understanding. Calls can be synchronous when they need to be. And the engine is now swappable: because the interface is C++ rather than baked into one runtime, Hermes replaced JavaScriptCore without rewriting the world.
The New Architecture
Three pieces on top of JSI, all enabled by default now.
Fabric is the new renderer. It keeps an immutable C++ tree of your UI shared by both sides, so layout can be computed on any thread and a view's dimensions can be read synchronously. In practice: fewer frames where a view is briefly the wrong size, and no round trip to measure something.
TurboModules are lazily initialised native modules. Under the old system every module loaded at startup; now a module loads the first time you touch it, which is most of the startup win.
Codegen generates the C++ glue between your TypeScript types and the native interfaces at build time, so a mismatch is a compile error instead of an undefined at runtime.
What this means for ordinary code is: almost nothing. You write the same components. It matters when you write a native module, when you read a library's changelog, and when you wonder why an app feels faster than it used to.
Hermes
Hermes is optimised for the thing a mobile app actually does — start quickly on a device with a cold cache — rather than for peak throughput on a long-running benchmark.
The main trick is ahead-of-time bytecode. Your JavaScript is compiled to Hermes bytecode during the build, so the app starts by loading bytecode rather than parsing source. On a large bundle that is a large fraction of startup time.
It also uses less memory and produces a smaller download than shipping JavaScriptCore.
The practical consequences show up in ordinary code. Hermes ships a full ICU-backed
Intl, so date and currency formatting work as they do in a browser — but that was not
always true, which is why careful code still guards it. Some very new language features arrive later
than in V8. And there is no Web Crypto: crypto.randomUUID() is simply undefined, which
is why the demo app uses expo-crypto rather than reaching for
Math.random().
Two threads, and where your code is
This is the practical model to keep in your head.
The JavaScript thread runs your components, effects and handlers. The UI thread lays out and draws. If the JavaScript thread is busy, the app is not necessarily janky — a scroll driven natively stays smooth — but anything waiting on your code is stalled.
That single fact explains several rules from earlier lessons. useNativeDriver: true
hands an animation to the UI thread so a busy JavaScript thread cannot stutter it. Reanimated goes
further and runs your animation logic there. And the dev menu's performance monitor shows
both frame rates separately for exactly this reason: which one is dropping tells you where to
look.
How your code gets to the device
Metro is the bundler. It resolves modules — including the
.ios.tsx/.android.tsx/.web.tsx platform extensions — transforms
them with Babel, and produces one bundle per platform. In development it serves that bundle over
HTTP, which is what makes fast refresh work and why a physical device needs to reach your machine
over the network.
In a release build the bundle is compiled to bytecode and embedded in the binary. That is why a production build behaves differently enough that profiling a debug build tells you very little.
What is next
Building and Releasing to the App Stores — the last mile, and the part web developers underestimate.