How we made Hybrid iOS apps compile 39% faster with React Native Brownfield

React Native Brownfield lets you embed React Native (and Expo) screens into an existing native iOS or Android app, then ship that integration as a distributable library: an XCFramework on iOS, an AAR on Android. The brownfield package:ios command drives most of that work: it runs the Xcode build, collects Hermes, Brownfield helpers, and your app framework, and writes everything under ios/.brownfield/package/build/.

For a long time, one step in that pipeline remained problematic for iOS: compiling React Native itself from source. React Native has been shipping prebuilts for Android long before, but iOS remained problematic, making us (devs) feel frustrated waiting for build times, and wasting CI time.

However, starting with React Native 0.83 and Expo 55, React Native started shipping with Apple prebuilts. Instead of waiting minutes for React Native to be compiled locally, the NPM package contains precompiled XCFrameworks for the RN core (React, ReactNativeDependencies) that CocoaPods can link instead of building thousands of C++ and Objective-C files on every packaging run. React Native 0.81 made prebuilts opt-in; from 0.84 they are the default upstream. Brownfield 3.10+ supports React Native 0.83 and later.

In Brownfield projects, prebuilts start with React Native 0.83 as an opt-in feature, then became the default in higher versions. In the case of Expo, versions ≥ 55 are using prebuilts by default.

Brownfield 3.10.0 (PR #323) wires those prebuilts into the packaging flow, and does a bunch of other stuff (described below). The Brownfield CLI & Expo config plugin are already version-aware and use appropriate defaults (see the Brownfield docs section for React Native & section for Expo).

Prebuilts do not change the Brownfield integration model. Your host app still loads an XCFramework and presents React Native like any other native module. Prebuilts remove a large, repeatable compile step from the packaging path. For teams shipping brownfield bundles through CI, that is often the difference between a tolerable pipeline and one that blocks iteration.

What is packaged with Brownfield?

A typical Brownfield iOS package contains:

  • <YourFramework>.xcframework - your React Native app, bundled JS included
  • ReactBrownfield.xcframework - the Brownfield runtime (view controllers, host setup)
  • hermesvm.xcframework - the Hermes VM
  • Optional Brownie.xcframework, BrownfieldNavigation.xcframework, and Expo support frameworks when those packages are in the project

When prebuilts are enabled, two more artifacts appear:

  • React.xcframework
  • ReactNativeDependencies.xcframework

The native host app links them directly, so that during compilation time, the Xcode build skips compiling RN core sources and instead pulls React-Core-prebuilt through CocoaPods (inside RN internally, this is controlled by the RCT_USE_PREBUILT_RNCORE=1).

Enabling prebuilts

The CLI exposes an explicit switch:

npx brownfield package:ios --use-prebuilt-rn-core

You can pass true, false, or omit the flag entirely. Below is a summary of the relation between RN versions and the default behavior:

Project type RN / Expo version Default
Vanilla RN < 0.81 Unsupported
Vanilla RN 0.81 - 0.83 Off by default; pass --use-prebuilt-rn-core to opt in
Vanilla RN ≥ 0.84 On by default
Expo < SDK 55 Unsupported
Expo ≥ SDK 55 On by default

If you try to enable prebuilts on an unsupported version, don't worry: the CLI will fail early before running the build, so you don't end up with a cryptic Xcode error halfway through a 500-second build.

What changed under the hood

Prebuilts were not a single-flag change. A few changes were made to the packaging flow to make it work with prebuilts.

CocoaPods & Xcode build flags

On the CocoaPods side, ReactBrownfield.podspec conditionally adds the prebuilt dependency:

if ENV['RCT_USE_PREBUILT_RNCORE'] == '1'
  spec.dependency 'React-Core-prebuilt'
end

Brownfield also sets BUILD_LIBRARY_FOR_DISTRIBUTION, SWIFT_EMIT_MODULE_INTERFACE, and related flags on ReactBrownfield, Brownie, and BrownfieldNavigation podspecs so Release builds emit .swiftinterface files. That is required for xcodebuild -create-xcframework to merge device and simulator slices reliably.

@rpath install names

Expo-generated framework targets were producing dylibs with absolute install names instead of @rpath/.... At runtime, this would cause the host app to only work on the machine that built the libs (#310).

Therefore, we implemented a fix in the Expo config plugin to inject the following build settings:

DYLIB_INSTALL_NAME_BASE = "@rpath"
LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)"

on the Brownfield framework target so packaged artifacts use relocatable install names.

Interface-only helper frameworks

When the host app embeds both your main framework (which already contains linked symbols) and separate Brownie or BrownfieldNavigation XCFrameworks, you can hit duplicate symbol errors at link time.

Brownfield strips the binary from those helper XCFrameworks after creation, leaving interface-only frameworks: Swift can import Brownie, but the implementation comes from the main library. The same stripping applies to {scheme}.xcframework when you pass --scheme.

Analysis of build times before vs after

We benchmarked brownfield package:ios on the repository demo apps, comparing from-source and prebuilt runs on identical hardware. Each configuration was repeated 10 times, with the first run being a cold build (from scratch). Reported p-values are in the sense of the Mann-Whitney U test (α = 0.05).

If we take a look at all run times, including the cold & subsequent builds, we get:

App From-source (mean) Prebuilt (mean) Build time Δ p-value
ExpoApp55 503 s 305 s −39.3% 0.0091 (significant)
RNApp 218 s 155 s −29.0% 0.0493 (significant)

Cold starts (first run after a clean) dominate wall time on Expo projects. CocoaPods resolution, DerivedData, and Expo's larger native surface all add variance. Restricting to steady-state runs (run index ≥ 2) isolates the compile-time savings more clearly:

App From-source (mean) Prebuilt (mean) Build time Δ p-value
ExpoApp55 464 s 283 s −39.2% 0.0047 (significant)
RNApp 167 s 149 s −11.2% 0.0243 (significant)

The first thing that we may notice: in all scenarios, we’re seeing improvements of build time that are not only noticeable with a naked eye, but also statistically significantly better compared to the legacy from-source baseline.

Expo sees the largest gain because its dependency graph is heavier; vanilla RNApp still benefits, but a smaller share of total time is spent in RN core compilation once the rest of the pipeline is warm. If we compare percentage build time gains, we see that for Expo, they are consistent across cold and subsequent builds. For vanilla React Native, we get an ~11% boost in subsequent builds, compared to a ~29% boost in all builds.

Try it

Upgrade to @callstack/react-native-brownfield ≥ 3.10.0 and run packaging with defaults on a supported project:

npx brownfield package:ios --scheme YourFramework --configuration Release

On RN 0.83 or when comparing builds, toggle explicitly:

# for Vanilla RN:
npx brownfield package:ios --use-prebuilt-rn-core true
npx brownfield package:ios --use-prebuilt-rn-core false

# for Expo:
npx brownfield package:ios --use-prebuilt-expo true
npx brownfield package:ios --use-prebuilt-expo false

Full flag behavior, embed checklist, and Expo notes are in the iOS integration docs.

Conclusions

For teams shipping hybrid apps, this is a straightforward way to remove friction from the iOS packaging flow. React Native prebuilts shorten a long compile step, and give teams back time on every packaging run. If you’re using React Native Brownfield 3.10+ and React Native 0.84+ (Expo 55+), you get this behaviour out-of-the-box. In React Native 0.83, it is worth enabling prebuilts and measuring in your own setup.

Insights

Learn more about Brownfield

cover

Aug 27Article

React Native in .NET MAUI: A Practical Brownfield Integration Guide

Learn how to embed React Native inside a .NET MAUI app with a brownfield setup. This guide covers runtime packaging, lifecycle wiring, navigation, and shared state across both runtimes.

cover

Aug 3Article

Migration to React Native in 2026 Starts With a Delivery Question

Choosing between brownfield and greenfield React Native migration? Learn where risk sits, when to use a checkpoint, and what to prove before committing.

cover

Jun 18Article

How We Made the Brownfield Gradle Plugin 22% Faster

See how task-driven Gradle optimization made the Brownfield Gradle Plugin 22% faster for clean Android release builds.

cover

Jun 3Article

AI-Supported Brownfield Migration to React Native

See how AI-supported workflows cut brownfield React Native evaluation from weeks to days by speeding up setup, planning, implementation, and QA.

cover

May 18Article

Unlocking Expo Updates in an Isolated Brownfield Architecture with SDK 55

Learn how to enable Expo Updates in an Isolated Brownfield setup with Expo SDK 55, including iOS packaging, Android patching, and OTA testing.

cover

Apr 23Talk

State of Brownfield in React Native

A practical look at Brownfield React Native adoption in mature native apps, architecture, tooling, and real-world tradeoffs.

cover

Apr 2Webinar

AI-Assisted Migrations to React Native: From Months to Days

Reduce migration time from months to days by using AI in real React Native workflows. Learn when to migrate incrementally and when starting fresh is the better option.

cover

Mar 31Article

Brownie: A Type-Safe Shared-State Workflow for Brownfield React Native

Brownie brings stable, cross-platform shared state to brownfield React Native apps with typed models for Swift and Kotlin.

cover

Nov 17Talk

Vega Developer Meetup

Learn how to integrate React Native into Android TV apps at the Vega Developer Meetup. Workshop, roadmap, and community talks, live in Munich.

cover

May 16Podcast

You Don’t Need a Rewrite, You Need React Native Brownfield

Discover how modern React Native Brownfield integration enables cross-platform power in native apps, without rewriting from scratch.

cover

Jan 13Podcast

Lessons from Shopify's Migration to React Native with Mustafa Ali

Learn how Shopify's migration to React Native led to 86% code unification, reduced build times, and seamless feature parity across platforms.

cover

Jun 14Podcast

Migration to React Native

The 13th episode of React Universe On Air (formerly known as The React Native Show) is fully focused on migrating from different technologies to React Native, the two most popular approaches to this process (greenfield and brownfield software development), and our state-of-the-art brownfield approach.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.