What changes when a React Native iOS app is built with Xcode's Release configuration instead of Debug?
answer
- where the JS comes from
- main.jsbundle inside the app
- Bundle React Native code and images phase
- #if DEBUG in bundleURL
- Dev Menu gone in Release
basics
~20 sA Release build embeds the JavaScript as main.jsbundle inside the app, loads it from there instead of from Metro, compiles the JS in production mode and removes the Dev Menu. Archives for the App Store always use Release.
solid answer
~40 sIn a React Native iOS project the Xcode build phase **Bundle React Native code and images** runs `react-native-xcode.sh`. For a non-Debug configuration it bundles the JS with `--dev false` into `main.jsbundle` and copies it, with image assets, into the app. The app's `bundleURL()` switches on `#if DEBUG`: Debug asks Metro for the `index` bundle, Release loads `main.jsbundle` from the app bundle, so the app runs with no Metro connection. The React Native docs add that Release disables the in-app Dev Menu. `Product > Archive` uses Release, and you can test it locally with `npm run ios -- --mode="Release"` or by setting the scheme's Run build configuration to Release. Note that Debug also embeds a bundle when building for a physical device; it only skips bundling for the Simulator.
code
bash · 8 lines# Build and run the Release configuration from the project root
npm run ios -- --mode="Release"
# Inside the 'Bundle React Native code and images' build phase:
# skip bundling only for Debug, never for Release
if [ "${CONFIGURATION}" == "Debug" ]; then
export SKIP_BUNDLING=true
figo deeper
Remember that Release embeds main.jsbundle and runs without Metro, that the Dev Menu is gone, and how to run Release locally with --mode="Release".
Explain the Bundle React Native code and images phase, the #if DEBUG switch in bundleURL, and why Debug bundles on a device but not on the Simulator.
Diagnose a blank Release launch from the build log, and insist on testing Release on a device before archiving because optimized native code and production JS can behave differently.
Decide which configurations the team ships and tests, for example a staging configuration that bundles like Release, and keep build-phase edits reviewed because they silently affect every archive.
## Two configurations, two sources of JavaScript An Xcode project has **build configurations**; a React Native app ships with `Debug` and `Release`. The configuration decides compiler settings for native code, but for a React Native developer the bigger difference is **where the JavaScript comes from**. | | Debug | Release | |---|---|---| | JS source at runtime | Metro dev server, `index` bundle | `main.jsbundle` inside the app | | JS build mode | Development (`--dev true`) | Production (`--dev false`) | | Dev Menu | Available | Disabled | | Needs Metro running | Yes | No | | Used by `Product > Archive` | No | Yes | ## How the bundle gets into the app Every React Native iOS target has a shell-script build phase named **Bundle React Native code and images**. It runs `react-native-xcode.sh` from the `react-native` package, which reads Xcode's `CONFIGURATION` and `PLATFORM_NAME`: 1. **Debug on the Simulator:** bundling is skipped, because Metro serves the code. Setting `FORCE_BUNDLING` overrides this. 2. **Debug on a physical device:** the script bundles anyway, with development settings, so the device can start without the computer. Setting `SKIP_BUNDLING` in the build phase saves that time, a tip the React Native docs give. 3. **Any other configuration, including Release:** the script bundles with `DEV=false`, writing `main.jsbundle` (the default `BUNDLE_NAME` is `main`) plus the images your code `require()`s into the app's resources. ## How the app chooses which bundle to load The template's `AppDelegate` decides at compile time: ```swift override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } ``` The `DEBUG` flag is defined for the Debug configuration only, so a Release build never looks for Metro. If `main.jsbundle` is missing, because bundling was skipped or failed, the app has nothing to run. ## Other effects of Release - **The Dev Menu is disabled**, so users cannot open developer tools by shaking the phone. - **Production JS**: `__DEV__` is `false`, development-only warnings and checks are dropped, and the bundle is minified. - **Optimized native code**: Swift and Objective-C are compiled with optimizations, which is also why some bugs only appear in Release. ## Testing Release before you archive The docs describe two ways: - In Xcode, **Product > Scheme > Edit Scheme**, select the **Run** tab and set **Build Configuration** to `Release`, then build and run. - From the project root, `npm run ios -- --mode="Release"`. A Release build on a device keeps working after you unplug it and stop Metro, which is the quickest proof that the bundle is embedded. ## Release-only behaviour worth expecting Because both the native and the JS side change, some behaviour only shows up in Release: - **LogBox is gone.** Development warnings and error overlays are dev-only, so an error that showed a red box in Debug may simply crash or render nothing. - **`__DEV__` branches are removed.** Code guarded by `if (__DEV__)` never runs, which surprises teams that hid setup code behind it. - **`console.log` still runs** unless a build step strips it, so noisy logging costs time on the JS thread in production too. - **Timing changes.** Optimized native code and a minified bundle start faster, which can expose race conditions that Debug's slower start hid. ## The scenario: a parking app's first release build A team building a parking app has only ever run Debug. Their first Release run on a device shows a blank screen: a custom build phase edit set `SKIP_BUNDLING` for every configuration instead of only Debug, so `main.jsbundle` was never produced. Restoring the `if [ "${CONFIGURATION}" == "Debug" ]` guard fixes it, and the archive then carries the bundle. ## Common misunderstandings - **"Release still needs Metro."** It does not; the bundle is inside the app. - **"Debug never embeds JS."** It does on a physical device; only the Simulator path skips bundling. - **"Release is just faster native code."** The JS source, JS mode and Dev Menu all change too.
- A React Native iOS Release build shows a blank screen on launch; what is the first thing to check?Whether `main.jsbundle` made it into the app. In Release, `bundleURL()` loads only that file, so a skipped or failed bundling step leaves nothing to run. Check the **Bundle React Native code and images** phase output in the build log, and look for `SKIP_BUNDLING` set without a Debug-only guard.
- Why does a React Native Debug build on a physical iPhone take longer to build than on the Simulator?`react-native-xcode.sh` skips bundling for Debug on the Simulator, because Metro serves the code, but still bundles for a physical device so the app can start without the computer. The React Native docs suggest exporting `SKIP_BUNDLING` for Debug in the build phase to save that time.
saying these in an interview costs you the question
- A Release build still loads its JavaScript from Metro.
- Debug builds never embed a JS bundle, even on a physical device.
- The only difference in Release is optimized Swift and Objective-C.
- The Dev Menu stays available in Release for support staff.
- Product > Archive uses whatever configuration the Run action uses.