react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesplatform-native · verified 2026-09-24 · react-native

Native modules & the New Architecture

reviewedconfidence: hightrack: weakenedthis tier holds 83% on the public scorecard (6/11 graded · 0 overturned) →

related decisions: animation · lists · styling

cited by: media · ondevice-ai

re-verified 8× — 2026-09-24 · 2026-09-21 · 2026-09-10 · 2026-08-07 · 2026-07-16 · 2026-07-13 +2 · changelog

recommendation

Target the New Architecture (the only runtime since RN 0.82). Expo Modules for most native needs; Turbo Modules for standard custom native with zero dependencies; Nitro Modules only for performance-critical custom native where you accept pre-1.0 churn. What changes the pick is how chatty the JS↔native boundary is and how much version churn you can absorb.

  • most native integrations → Expo Modules (managed, fast path)
  • new Expo module on the SDK 58 beta → Expo Modules 2.0 API (@ExpoModule/@JS; beta, docs still being written, views not covered — else the 1.0 DSL)
  • perf-critical custom native (camera, audio, BLE) → Nitro Modules (pre-1.0, third-party, not in RN core: pin versions; 0.37 alone moved Props into Nitro core and renamed CachedProp to ReactProp)
  • standard custom native → Turbo Modules / Fabric (zero-dep, in-core, stable)
  • still on Legacy Arch → New Architecture migration (Legacy is frozen and stripped from core; budget the long tail, see the Discord reading)
  • RN >= 0.86 with react-native-edge-to-edge installed → core edge-to-edge (drop the standalone package; below 0.86 it is still required)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
Expo modulesManaged, fast path for most native needs, in Swift/Kotlin. SDK 56 made calls 1.5–2× faster than SDK 55 (on par with Turbo Modules); Expo Modules 2.0 (beta on iOS + Android in the SDK 58 beta) is a further 2.5–5.6× on sync iOS calls and beat a TurboModule on all twelve Android microbenchmarks. See note: EXPO MODULES 2.0.12.0M/wk · ships in 4/14
Turbo Modules / Fabric (JSI)Custom native on the New Architecture; zero-dep, in RN core and stable, which is why it stays the pick when Nitro's pre-1.0 churn is not worth the speed.
Nitro ModulesCodegen-driven JSI, positioned as Turbo Modules' successor; vendor claims ~10x vs Turbo, ~100x vs Expo Modules — the Expo half is STALE (see note: EXPO MODULES 2.0). Pre-1.0 and third-party: 0.37.1 (2026-08-27, verified vs npm), still shipping breaking changes, so pin versions. See note: NITRO, STEWARDSHIP.2.2M/wk · ships in 9/14

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

npm weekly downloads (from the corpus's last signals run): react-native-nitro-modules 2.2M · expo-modules-core 12.0M · expo 12.0M

Verified notes

THE PICTURE: the New Architecture (Fabric + Turbo Modules over JSI, no async bridge) is the only runtime, so the choice is which authoring layer sits on JSI. Expo Modules is the default because its call overhead is no longer a tax: Expo's own numbers put it level with Turbo Modules on SDK 56 and ahead of a TurboModule on Android with Expo Modules 2.0. Turbo Modules stay the stable in-core option. Nitro Modules buys the lowest overhead at the price of pre-1.0 churn, which only pays off for chatty, fine-grained boundaries (camera frames, audio, BLE). ARCHITECTURE (verified vs the RN 0.84 blog): the New Architecture was the default for new projects from RN 0.76, became the ONLY runtime option in RN 0.82, and RN 0.84 keeps stripping Legacy-Arch code from iOS + Android; the bridge interop layer was fully removed in RN 0.85 → all new native work targets it. EXPO MODULES 2.0 (2026-09-21, verified vs the SDK 58 beta changelog of 2026-09-15; API from the 2026-09-08 Expo blog post): the authoring API becomes annotated plain Swift/Kotlin (@ExpoModule on a class, @JS on methods and properties, @Record on a struct, a typed callable property for events), retiring the 1.0 result-builder DSL (Name/Function/AsyncFunction/Property/Class/Events); sync-vs-async is decided by Swift's own `async` keyword. It is IN BETA ON BOTH iOS AND ANDROID in the SDK 58 beta; documentation is still being written (the early-look post remains the guide) and views are not covered. iOS: on the same SDK the @JS path is 2.5–5.6× faster than 1.0 for SYNCHRONOUS calls (async roughly unchanged; iPhone 16 Pro / iOS 27 / Release, 100k calls) because the macro reads the signature at build time and the 1.0 reflection path disappears. Android: across twelve microbenchmarks 2.0 beat 1.0 in every case (1.2× for an array of 100 numbers up to 39.9× for emitting an event with a number) and beat a TurboModule in every case (no-arg call 112 ns vs 2,837 ns; array of 20 objects 10.7 µs vs 149.8 µs; event with an object 963 ns vs 8,080 ns) → on Android call overhead 2.0 is ahead of Turbo Modules, not level. These are vendor-run call-overhead microbenchmarks: they matter for chatty boundaries and are irrelevant to a module that makes one call and does real work behind it. Migration is incremental (both APIs merge in one module; the `expo-migrate-module` agent skill converts the Swift side and reports what it left behind); Expo's stated rationale is that models know Swift/Kotlin, not a bespoke DSL. BUILD TIME (2026-09-21, verified vs the SDK 58 beta changelog): expo-modules-core ships PRECOMPILED .so files for all four ABIs in debug and release → clean builds and CI no longer compile its C++; Expo's blank-project clean four-ABI Android debug build on an M3 Pro went ~80s → ~39s, with a larger saving on low-core CI runners. EXPO SDK 56 (2026-07-09, verified vs its changelog): expo-modules-core dropped the Objective-C++ intermediary for DIRECT Swift↔C++ JSI interop ("fewer hops means less call overhead"), making calls 1.5–2× faster than SDK 55 and, per Expo, "on par with React Native's Turbo Modules"; SDK 56 also allows inline Expo Modules (Swift/Kotlin in-app with generated TypeScript). Expo Modules ships SHARED OBJECTS/REFS (verified vs docs.expo.dev/modules/shared-objects): JS handles to live native objects instead of serialized copies; the next-gen MediaLibrary/Contacts APIs use them. NITRO (verified vs the v0.37.0 release and npm): v0.37.0 (2026-08-20) moved Props and react::ComponentDescriptor into Nitro core via C++ templates and renamed CachedProp to ReactProp (old name kept as a deprecated alias) → the churn is real; this entry's tripwire fires at 1.0. Nitro is consolidating custom native: mmkv v4, device-info, image, video, fetch, healthkit and geolocation moved to it, and VisionCamera v5 rewrote ~3,000 lines of JSI/C++ onto it. STEWARDSHIP (announced 2026-09-01, verified 2026-09-21 vs Margelo's announcement): Margelo, the agency behind Nitro Modules, VisionCamera and react-native-mmkv, joined Callstack in a transaction valuing Margelo at more than €20 million; the existing open-source libraries remain open source and Marc Rousavy continues to lead them → continuity, not a handover risk; the "small independent vendor" half of Nitro's risk shrank, the pre-1.0 half did not. Contrast RB-E-ANIMATION (Skia) and RB-E-LISTS (FlashList), where Shopify is leaving; RB-E-STYLING owns Tailwind into Shopify. EDGE-TO-EDGE (verified vs the RN 0.86 blog): RN 0.86 (2026-06-11) moved Android-15+ edge-to-edge into core, fixing KeyboardAvoidingView / Dimensions / StatusBar → on RN >= 0.86 drop the standalone react-native-edge-to-edge (zoontek); below 0.86 it is still required. LEAD (2026-07-16 back-audit; TWiR #284 and #285): margelo/react-native-runtimes runs RN components/business logic in ISOLATED Hermes runtimes; young, not an option until adoption shows. HISTORY: 2026-09-24 CORRECTED: the Expo Modules 2.0 reading claim still said iOS-only and undocumented in SDK 57. 2026-09-21 REOPENED: Expo Modules 2.0 in beta on iOS and Android (SDK 58 beta), measured ahead of a TurboModule on Android call overhead; precompiled expo-modules-core halves clean Android builds; Margelo joined Callstack. 2026-09-10 REOPENED: Expo Modules 2.0 early look (iOS-only, undocumented at the time). 2026-07-09 REOPENED: Expo SDK 56 direct Swift↔C++ JSI interop, inline modules. 2026-06-25 CHALLENGED → SURVIVES: adversarial stress test; Nitro stability caveat added (pre-1.0, third-party; Turbo Modules stay the stable in-core option).

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

React Native Bridgeless: race conditions with concurrent JS runtimesOscar Franco (op-sqlite author)

A native-module authoring rule extracted from a real op-sqlite crash, and it invalidates an assumption most C++ modules were written under. Under the old bridge a reload was strictly sequential — the outgoing runtime's invalidate() completed before the next generation's install() ran — so process-global module state (a static shared_ptr, a bool flag, a cached CallInvoker) was safe by construction, because only one generation could ever touch it. Bridgeless starts the new RCTInstance BEFORE the old one has finished invalidating and gives the old one only a few seconds to drain, so two runtime generations are genuinely alive at once. The crash: an in-flight SQLite query on a background thread is interrupted by a reload or OTA update and tries to schedule its error back onto the JS thread, by which time the new generation has already overwritten the global invoker — so the callback either runs on the wrong invoker or destroys JS objects off the JS thread. The fix generalizes past SQLite: give each generation its own liveness flag (a shared_ptr<atomic<bool>> that install() allocates and invalidate() flips for THAT generation only), and have every host object COPY the invoker and the liveness pointer into members at construction rather than reading the globals inside the callback — bind generation identity at creation, check it at run time. Fixed every update hook at once with no call-site changes.

What It Actually Takes to Migrate Discord to React Native's New ArchitectureSoftware Mansion (embedded ~1 year with Discord's iOS team)

The post-flip half of a New-Architecture migration, which the official docs don't cover, told through one app at Discord's scale (Android first, then iOS). The headline is the ticket breakdown on the way back to parity: rendering/layout/visual 47%, crashes & stability 16%, build/infra/the migration ITSELF 14%, performance 13%, input (keyboard/gesture) 11% — i.e. flipping the switch is about one seventh of the work, and the rest is the app behaving differently under the new renderer. Three broken implicit contracts, each worth knowing before you hit it: (1) MEASUREMENT ORIGIN — old arch measured a view against the window, Fabric measures against its Yoga root, and a modal is its own root, so the same measure() returns different numbers inside a modal; the tell is a constant offset that looks like the height of something else. (2) VIEW IDENTITY — Fabric's view flattening is re-evaluated on every commit, and accessibility props are among those that force a real host view, so toggling accessibilityElementsHidden mid-gesture can swap the gesture's backing view and cancel it silently (onFinalize never runs); collapsable={false} pins it. (3) NATIVE LOOKUP — the legacy-interop layer finds a view manager by appending 'Manager' to the component name; a class named without that suffix misses the fast path, falls back to a main-thread-synchronous module creation while holding a registry write lock, and deadlocks against an in-flight animation's read lock. The advice that generalizes: budget for INVESTIGATION, not fixes (every story above ended in one prop, one helper, or a rename — the cost was finding it); the problems cluster where your app touches native (measurements, refs, gestures, imperative calls, interop-layer components) while pure React code comes through untouched; instrument before optimizing; and fix upstream rather than patch locally, because a local patch is tech debt every future upgrade must re-check.

An early look at Expo Modules 2.0Tomasz Sapeta (Expo)

The first-party walkthrough of the API change behind this entry's Expo Modules 2.0 note: each 1.0 DSL component folded into ordinary Swift (Function/AsyncFunction → a method marked @JS, Property → a var, a record → a struct, an event → one typed callable property), with the migration path (both APIs coexist in one module; the expo-migrate-module agent skill does the Swift side and lists what it couldn't). Also states the mechanism behind the speedup — build-time signatures replace 1.0's per-call reflection — and what is NOT done yet: views, Android, generated TypeScript declarations, and documentation.

How to Make Pure JSI Code Faster in React Native (Part 1)Alex Shumihin (Margelo)

How JSI bindings actually work at the C++/Hermes boundary — HostFunction vs HostObject, the Object+NativeState pattern Nitro compiles down to, stack vs heap, minimizing JS-to-native crossings — with reproducible Hermes benchmarks. The runtime mechanics under Turbo/Nitro Modules.

Making JSI Faster with More Efficient Data Structures (Part 2)Alex Shumihin (Margelo)

How data representation across the JSI boundary (array-of-objects vs flat array vs ArrayBuffer, numeric vs string contracts) can swing performance up to ~30x with no algorithmic change. First-principles guidance for New-Architecture native modules.

How Margelo Helped Discord Improve React Native's New Architecture PerformanceHanno Gödecke (Margelo)

Low-level New-Architecture jank postmortem: Reanimated's cloneShadowTreeWithNewProps over-cloning the Fabric shadow tree plus redundant layout passes for non-layout props, with fixes upstreamed. Canonical Fabric / New-Arch internals.

Four Years of React Native Quick Crypto: From Wallets to Node ParityBrad Anderson (Margelo)

A four-year native-library architecture case study: wrapping OpenSSL for Node behavior-parity, a sync-first threading model with async escape hatches, the hand-written-JSI to Nitro codegen migration, and security audit as architecture validation. Generalizable lessons for native-module authors.

The Memory Hermes Can't See: Stale Shadow Nodes in React NativeTomasz J. Żelawski (Software Mansion)

The memory-leak sibling of the Discord jank postmortem above: Fabric ShadowNodes are C++ objects retained via JS wrapper objects, so unmounted views' wrappers can pin whole shadow-tree revisions (amplified by Reanimated's per-frame revision cloning) — and none of it appears in Hermes heap snapshots, because the retained memory sits outside the JS heap. The author's two candidate mitigations (periodic forced GC; per-wrapper setExternalMemoryPressure) are both judged not production-ready — this is debugging awareness, not a shipped fix. (2026-08-05)

Watching

Curated talks, podcasts & videos — the A/V companion to the reading list.

RNR 351 — Transforming Packages to Nitro with Marc RousavyReact Native Radio (guest: Marc Rousavy, Nitro/VisionCamera author)

37-min episode (2026-01) from Nitro's author on why Nitro exists (JSI boilerplate), how it compares to Turbo Modules and Expo Modules, and the ecosystem's package-migration wave — the audio companion to this entry's Nitro-consolidation note.

Sources

Depth (in-domain rules) is owned by the react-native-jsi skill — this entry is selection breadth.

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About native modules & the New Architecture (React Native)

Diataxis: Explanation. This page builds understanding of how RN talks to native code and which module path to choose. It is not a tutorial: the candidate list lives in the index entry RB-E-NATIVE; JSI memory-ownership and threading rules are owned by the react-native-jsi skill. Read this for the why; read that skill to do it safely.

The shift that changed everything: the bridge is gone

For years React Native's JS and native sides talked over an asynchronous bridge that serialised every call to JSON. That bridge was the source of most "RN is slow at the boundary" pain: batched, async, copy-heavy. The New Architecture removes it. JS and native now share a C++ layer — JSI (the JavaScript Interface) — and call each other synchronously, without serialization. On top of JSI sit Fabric (the renderer, with an immutable C++ shadow tree) and Turbo Modules (native modules over JSI). This is the single fact that organises every native decision: there is no async bridge anymore; native is a synchronous, C++-mediated call away.

Two consequences: native calls can be synchronous and cheap, and a whole class of native libraries can be rebuilt on JSI for large speedups. The cost is that the boundary is now real C++ — memory ownership and threading matter (which is why depth defers to react-native-jsi).

This is not optional anymore

The New Architecture is the architecture. It became the default for new projects in RN 0.76, the only runtime option in RN 0.82 (opt-out ignored), and RN 0.84 began stripping Legacy-Architecture code from iOS and Android. Legacy was frozen at 0.80 and is effectively gone. So "should we migrate to the New Architecture?" is no longer a question — all new native work targets it, and apps still on Legacy are on borrowed time (RB-E-RN-VERSIONS). (Verified against the official RN 0.84 and 0.86 release blogs.) One dependency falls out of this too: RN 0.86 moved Android-15+ edge-to-edge into core, so on RN 0.86+ the standalone react-native-edge-to-edge can be dropped; below 0.86 it is still needed.

The default, and why

Target the New Architecture (the only runtime since RN 0.82). Expo Modules for most native needs; Turbo Modules for standard custom native with zero dependencies; Nitro Modules only for performance-critical custom native where you accept pre-1.0 churn.

The selection is about how much native you're writing and how hot the path is. Expo Modules is the managed fast-path: a clean Swift/Kotlin authoring model that covers the vast majority of integrations with the least ceremony — the right default even in bare RN. When you need a custom native module on a hot path (camera frames, audio, BLE, crypto), Nitro Modules — codegen-driven JSI, positioned as Turbo Modules' successor — is the performance choice and is consolidating the ecosystem (mmkv v4, device-info, image, video, fetch, and VisionCamera v5 all moved onto it). Turbo Modules / Fabric directly is the standard path when you're writing conventional custom native and don't need Nitro's edge.

Two facts moved this default in 2026. First, Expo Modules stopped being the slow option: SDK 56 dropped the Objective-C++ hop for direct Swift↔C++ JSI interop, making calls 1.5–2× faster and, per Expo, on par with Turbo Modules; Expo Modules 2.0 (annotated plain Swift/Kotlin — @ExpoModule, @JS, @Record — in beta on iOS and Android in the SDK 58 beta, docs still being written) is faster again, and on Android it beat a TurboModule on all twelve of Expo's call-overhead microbenchmarks. Those are vendor microbenchmarks of the boundary: they matter for chatty modules, not for one call that does real work. Second, Nitro is still pre-1.0 and third-party (0.37.x; 0.37.0 alone moved Props into Nitro core and renamed CachedProp to ReactProp), so pin its version; Turbo Modules stay the stable in-core option when that churn is not worth the speed. Its stewardship is steady: Margelo, which builds Nitro, VisionCamera and react-native-mmkv, joined Callstack (announced 2026-09-01), with the libraries staying open source and Marc Rousavy still leading them.

The landscape, and when each one wins

Expo Modules — managed, ergonomic, broad coverage; the default for most native needs. You write idiomatic Swift/Kotlin and get a clean JS surface. Not Expo-app-only — usable in bare RN.

Turbo Modules / Fabric (JSI) — the New Architecture's own module and rendering system. The baseline for custom native work; you're writing against codegen and JSI directly.

Nitro Modules — codegen JSI with an emphasis on raw performance and ergonomics; the ecosystem's consolidation point for perf-critical native. Reach for it when the boundary is the bottleneck — high-frequency calls, large buffers (ArrayBuffers to avoid serialization), real-time pipelines. The durable mechanics of why JSI code is fast (HostObject vs HostFunction, data-shape choices) are worth understanding — see the reading.

Tradeoffs and failure modes to name out loud

  • Hand-writing JSI you didn't need to. Most "I need native" cases are an Expo Module, not raw JSI. Drop to Turbo/Nitro/JSI only when a managed module genuinely can't do it or the path is hot.
  • Ignoring the C++ ownership model. With the bridge gone, the boundary is real memory and real threads; lifetime bugs and cross-runtime object passing are the new failure class. This is exactly what react-native-jsi owns — route there before shipping custom native.
  • Treating "New Arch migration" as optional. It isn't (mandatory 0.82, stripped 0.84). Budgeting it as a someday-task on a pre-0.82 app is a planning error (RB-E-RN-VERSIONS).
  • Serializing across a non-serialized boundary. A common Nitro/JSI lesson: passing big arrays-of-objects or strings where an ArrayBuffer/typed contract would do can swamp the gains. Data shape across the boundary is a first-order performance decision.

How it interacts with the rest of the stack

  • RN versions (RB-E-RN-VERSIONS). The New Architecture's mandatory/stripped timeline is the reason to track RN versions; this entry explains the architecture those versions ship.
  • Media (RB-E-MEDIA) & storage (RB-E-STORAGE). The flagship Nitro/JSI consumers: VisionCamera v5 (frame processors), MMKV v4, op-sqlite. Those entries are the domain; this one is the module system underneath.
  • Build (RB-E-BUILD). Codegen, precompiled iOS binaries, and Hermes V1 are the build-time side of the New Architecture.
  • JSI depth (react-native-jsi). Memory ownership, threading, and JSI API correctness live there — this page tells you which module path, that skill tells you how to not crash.

In one paragraph

The New Architecture deleted the async bridge: JS and native now share a synchronous C++ layer (JSI), with Fabric rendering and Turbo/Nitro Modules for native code. It's mandatory from RN 0.82 and Legacy is being stripped, so all new native work targets it. Choose by how much native you're writing: Expo Modules for most needs (managed, even in bare RN), Nitro Modules for performance-critical custom native (it's consolidating the ecosystem), Turbo Modules/Fabric for standard custom work — and push JSI memory/threading correctness down to react-native-jsi, because the boundary is now real C++.


See also: RB-E-RN-VERSIONS (the mandatory-migration timeline), RB-E-MEDIA / RB-E-STORAGE (flagship Nitro/JSI consumers), RB-E-BUILD (codegen, Hermes V1, precompiled iOS). JSI memory/threading depth: the react-native-jsi skill.

Related in platform-native: media · storage · native-ui · keyboard · payments · brownfield · games · alt-frameworks