Skip to content

[iOS][Fabric][Bridgeless] 0.86 Release-only boot hang: first commit of a react-navigation Tabs tree never mounts - all threads idle in mach_msg, JS event loop alive, no JS error #57511

Open

Description

@sjconnolly

Description

On React Native 0.86.0 (bridgeless / New Architecture, Hermes), iOS Release builds can hang on the native splash screen forever at cold launch: the first Fabric commit that mounts the navigation tree never completes, while every thread in the process sits idle and the JS event loop stays healthy. Debug builds with Metro connected never hang.

What tips it over differs by environment, which is why this looks like a timing race rather than a structural failure:

  • On physical devices (production app, iPhone 17 Pro Max + iPad Air M2, iOS 26.5): a bottom-Tabs app whose tab screens each host a nested native Stack (react-native-screens 4.25.2 via expo-router 57.0.4) hangs 10/10 launches; the identical app with the tab screens flattened boots 10/10. Even a bare nested directory route level (no Stack _layout) reproduced the hang there.
  • In the minimal repro (linked below): any variant using the classic Tabs navigator (react-navigation bottom-tabs) hangs - nested or flattened, navigator mounted in the very first commit or deferred one async tick, with or without instrumentation - on both the iOS 26.5 simulator and a physical iPad Air M2 (iPadOS 26.5.2). The stock create-expo-app default template (which uses NativeTabs) boots normally on the same simulator and Xcode. The repro is small and fast enough that any react-navigation Tabs tree loses the race; the full-size production app only loses it when a navigator is nested inside a tab.

Failure signature, identical in every hung configuration (instrumented Release builds, device and simulator):

  • The first Fabric commit never completes. React renders (JS executes past the navigator render), but effects that should fire after the navigator's first commit never run, no UI ever appears, and the native splash stays up indefinitely (left >10 minutes).
  • No JS error surfaces on any channel. An ErrorUtils global handler, a console.error wrap, and a globalThis.reportError wrap all stay silent for the entire hang.
  • The JS event loop stays alive. A setTimeout heartbeat chain started at bundle-execution time keeps firing once per second for as long as the hung app is left running, and TurboModule calls made from those timers keep working (the heartbeat writes a file through expo-file-system every second).
  • All threads are idle. Sending the hung process SIGABRT and reading the resulting thread dump shows no thread executing app, React, or Hermes code: the main thread is parked in mach_msg runloop wait, and com.facebook.react.runtime.JavaScript is likewise parked in mach_msg runloop wait. Nothing spins, and there is no visible lock cycle - it looks like a lost wakeup / boot-time race rather than a deadlock, consistent with it being Release-only (timing-dependent) behavior.

In the production app, flattening the tab screens fixed it with everything else unchanged: each tab becomes a plain screen (no nested Stack; the Tabs navigator renders headers itself). Same RN, same libraries, same app code otherwise - 0/10 boots before, 10/10 after. In the minimal repro the flattened branch still hangs, consistent with the race being timing-dependent rather than structural: nesting is one way to tip it over, a small fast bundle is another.

Cold-launch census (each run classified by marker files the app writes into its Documents directory - see the reproducer):

Environment (all Release, all cold launches) Structure Result
iPhone 17 simulator, iOS 26.5 stock create-expo-app template (NativeTabs, flat, immediate mount) boots - UI verified
iPhone 17 simulator Tabs + nested Stack per tab, splash-held mount (repro main) 0/10 mounted, 10/10 hung
iPhone 17 simulator Tabs flattened, splash-held mount (repro flattened) 0/10 mounted, 10/10 hung
iPhone 17 simulator Tabs flattened, navigator in first commit 0/5 mounted, 5/5 hung
iPhone 17 simulator Tabs flattened, first commit, all instrumentation removed hung (splash verified by screenshot)
iPad Air M2 (iPadOS 26.5.2, physical) repro main (nested) 0/10 mounted, 10/10 hung
iPad Air M2 (physical) repro flattened 0/10 mounted, 10/10 hung
iPhone 17 Pro Max + iPad Air M2 (physical), production app nested Stack per tab 0/10 mounted (shipped to TestFlight this way)
Same devices, production app flattened, same commit otherwise 10/10 mounted

In every hung simulator run the heartbeat file showed exactly 13 beats in the 13 seconds before classification and boot-error.txt was never written - the JS event loop is healthy, silently, forever.

The production data comes from a device bisect on 2026-07-10: a pristine failing build hung 0/10 launches; the same code with only the three per-tab stacks flattened booted 10/10 (the fix commit in our app deletes the per-tab Stack layouts and renders the tab screens flat - nothing else).

Ruled out during the production bisect (each variable toggled on-device, in Release, with the same launch census): splash-screen handling, react-native-reanimated, Hermes variant, RN feature-flag overrides, Metro inlineRequires, deferring the navigator mount behind an extra async tick, and JS-side scheduler watchdogs. None changed the outcome; only the navigator-inside-tab structure did. One triage caveat from that work: crashes can masquerade as hangs in a naive launch loop, so the census distinguishes them (a crashed process stops writing heartbeats; a hung one keeps writing them forever - these launches are all genuinely hung, not crashed).

I'm aware the issue template routes Expo apps to the expo repo - filing here because the observable failure is core Fabric behavior in a bridgeless Release build (first commit/mount never completes while both the main and JS threads sit idle in their runloops, with the JS event loop demonstrably healthy). The NativeTabs-boots vs Tabs-hangs split could equally implicate react-navigation or react-native-screens, but the hang state itself - a rendered-but-never-mounted tree with commit/mount scheduling idle - belongs to the renderer, and in the full-size production app the very same Tabs tree boots reliably on device once no navigator is nested inside a tab. Happy to have this re-routed if triage disagrees; a cross-referenced report is filed on expo/expo (link in the first comment).

Steps to reproduce

Simulator path (fastest):

  1. Clone the reproducer: https://github.com/sjconnolly/rn086-tabs-stack-hang-repro (stock create-expo-app default template for Expo SDK 57 / RN 0.86.0, restructured to 3 Tabs tabs that each host a nested native Stack, plus boot-marker instrumentation)
  2. npm install && npx expo prebuild -p ios
  3. xcodebuild -workspace ios/rn086tabsstackhangrepro.xcworkspace -scheme rn086tabsstackhangrepro -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath build-sim build
  4. tools/sim-census.sh <sim-udid> <path-to-built-.app> 10 - installs, cold-launches 10×, classifies each run from the marker files
  5. Observe: native splash forever on every launch; boot-js-start.txt written and boot-heartbeat.txt advancing every second for as long as the app is left running; boot-mounted.txt never written; boot-error.txt never written

Device path (mirrors how this was found; needs a signing team in app.json → ios.appleTeamId):

  1. Build the same workspace with -destination 'id=<device-udid>', install with xcrun devicectl device install app, then tools/launch-census.sh <device-udid> 10
  2. git checkout flattened (deletes the per-tab Stacks - the only change), rebuild: in this minimal repro the flattened variant still hangs on both device and simulator; in our full-size production app the identical flattening change took device boots from 0/10 to 10/10
  3. Debug builds with Metro never reproduce the hang (verified throughout the production bisect; the failure is Release-only)

React Native Version

0.86.0

Affected Platforms

Runtime - iOS, Build - MacOS

Areas

Fabric - The New Renderer, Bridgeless - The New Initialization Flow

Output of npx @react-native-community/cli info

System:
  OS: macOS 26.5
  CPU: (18) arm64 Apple M5 Max
Binaries:
  Node: 22.16.0
  npm: 10.9.2
  Watchman: Not Found
Managers:
  CocoaPods: 1.17.0
SDKs:
  iOS SDK:
    Platforms: DriverKit 25.5, iOS 26.5, macOS 26.5, tvOS 26.5, visionOS 26.5, watchOS 26.5
IDEs:
  Xcode: 26.6/17F113
Languages:
  Ruby: 4.0.5
npmPackages:
  react: 19.2.3
  react-native: 0.86.0
  expo: 57.0.4
  expo-router: 57.0.4
  react-native-screens: 4.25.2
  react-native-safe-area-context: 5.7.x
Runtime targets:
  iPad Air 11-inch (M2), iPadOS 26.5.2 (physical)
  iPhone 17 Pro Max, iOS 26.5 (physical, production app)
  iPhone 17 simulator, iOS 26.5
Hermes: enabled  |  New Architecture (bridgeless): enabled (SDK 57 default)

Stacktrace or Logs

There is no stack trace - nothing crashes and nothing errors. Below is a sample of a hung Release process on the iOS 26.5 simulator (1689 samples over 2s, all threads). SIGABRT crash reports forced from hung repro processes on both the simulator and the physical iPad (iPadOS 26.5.2), as well as the SIGABRT dump from our production bisect, all show the identical wait states. Every thread is idle. The only activity in the entire sample window is the display-link timer delivering our heartbeat setTimeouts to the JS thread:

Thread: DispatchQueue_1: com.apple.main-thread  (serial)
  1689 main → UIApplicationMain → -[UIApplication _run]
    1689 GSEventRunModal → _CFRunLoopRunSpecificWithOptions → __CFRunLoopRun
      1689 __CFRunLoopServiceMachPort → mach_msg → mach_msg2_trap   ← idle, all 1689 samples

Thread: com.facebook.react.runtime.JavaScript
  1689 +[RCTJSThreadManager runRunLoop] → _CFRunLoopRunSpecificWithOptions
    1681 __CFRunLoopRun → __CFRunLoopServiceMachPort → mach_msg → mach_msg2_trap   ← idle
       6 __CFRunLoopDoSource1 → __CFMachPortPerform → display_timer_callback
         → CA::Display::DisplayLink::dispatch_items → -[RCTDisplayLink _jsThreadUpdate:]   ← our 1 Hz heartbeat timers

Thread: hades (Hermes GC)
  1689 hermes::vm::HadesGC::Executor::worker() → std::condition_variable::wait   ← idle

No thread is executing React, Fabric, or app code. No lock cycle is visible. The first commit's mount work appears to have been scheduled and lost - consistent with a Release-only (timing-dependent) lost-wakeup race during bridgeless boot.

MANDATORY Reproducer

https://github.com/sjconnolly/rn086-tabs-stack-hang-repro

Screenshots and Videos

The app sits on the native splash indefinitely; there is nothing else to see. Screenshots in the reproducer repo: hung repro on splash vs stock template booted on the same simulator.

Census output for the repro main branch on the iOS 26.5 simulator (tools/sim-census.sh, 15s wait per launch):

run 1: HUNG (js-start=2026-07-10T16:08:59.448Z heartbeat=13 2026-07-10T16:09:12.666Z mounted=MISSING error=MISSING)
run 2: HUNG (js-start=2026-07-10T16:09:14.889Z heartbeat=13 2026-07-10T16:09:28.099Z mounted=MISSING error=MISSING)
run 3: HUNG (js-start=2026-07-10T16:09:30.418Z heartbeat=13 2026-07-10T16:09:43.633Z mounted=MISSING error=MISSING)
run 4: HUNG (js-start=2026-07-10T16:09:46.045Z heartbeat=13 2026-07-10T16:09:59.249Z mounted=MISSING error=MISSING)
run 5: HUNG (js-start=2026-07-10T16:10:01.539Z heartbeat=13 2026-07-10T16:10:14.749Z mounted=MISSING error=MISSING)
run 6: HUNG (js-start=2026-07-10T16:10:17.085Z heartbeat=13 2026-07-10T16:10:30.300Z mounted=MISSING error=MISSING)
run 7: HUNG (js-start=2026-07-10T16:10:32.639Z heartbeat=13 2026-07-10T16:10:45.850Z mounted=MISSING error=MISSING)
run 8: HUNG (js-start=2026-07-10T16:10:48.214Z heartbeat=13 2026-07-10T16:11:01.433Z mounted=MISSING error=MISSING)
run 9: HUNG (js-start=2026-07-10T16:11:03.765Z heartbeat=13 2026-07-10T16:11:16.983Z mounted=MISSING error=MISSING)
run 10: HUNG (js-start=2026-07-10T16:11:19.275Z heartbeat=13 2026-07-10T16:11:32.483Z mounted=MISSING error=MISSING)

census: 0/10 mounted, 10/10 hung on splash, 0/10 dead

js-start written means the bundle executed; heartbeat=13 means the JS event loop delivered all 13 one-second timer ticks during the 13s wait; mounted=MISSING means the first screen's useEffect never ran; error=MISSING means ErrorUtils/console.error/reportError never fired.

Activity

  1. sjconnolly commented on Jul 10, 2026

    @sjconnolly
    Author

    Cross-referenced expo-router report (same bug, Expo-side routing per the template guidance): expo/expo#47687

  2. changed the title [-][iOS][Fabric][Bridgeless] 0.86 Release-only boot hang: first commit of a react-navigation Tabs tree never mounts — all threads idle in mach_msg, JS event loop alive, no JS error[/-] [+][iOS][Fabric][Bridgeless] 0.86 Release-only boot hang: first commit of a react-navigation Tabs tree never mounts - all threads idle in mach_msg, JS event loop alive, no JS error[/+] on Jul 10, 2026
  3. nassimb commented on Sep 7, 2026

    @nassimb

    Reproducing this on a real, currently-shipping app — adding a data point that may narrow the search space, since the trigger here is not tab/route nesting.

    Environment

    expo: 57.0.20
    react-native: 0.86.3
    react-native-screens: 4.26.2
    Hermes: enabled, New Architecture (bridgeless): enabled (SDK 57 default)
    iOS: 26.6.1 (build 23G83), physical iPhone 13 (iPhone14,5)
    Xcode: 26.6 (17F113)
    

    Symptom

    Native splash stays up indefinitely on cold launch of a Release (TestFlight and locally-built, both distribution- and development-signed) build. Console shows the last native log line as:

    ReactInstance: evaluateJavaScript() with JS bundle
    

    ...and then nothing further, ever (left running 10+ minutes in several runs). No JS-side console.log — not even a single top-level synchronous statement placed at the very top of the root _layout.tsx — ever reaches the OS log. No crash, no NSException, no watchdog "hang" report generated (Settings → Analytics Data shows nothing new after repeated reproductions). xcrun devicectl device process launch --console confirms the process itself is still alive and simply never proceeds.

    What narrows this down: reproduces on a route-less, nav-less, tab-less app

    To isolate whether this was app-specific, I:

    1. Stripped the production app's root _layout.tsx down to a single <Text> render with zero imports beyond react-native — bundle size dropped from ~9MB to ~2.3MB (confirmed via strings on the built bundle). Still hung, identically.
    2. Removed every other file under app/ (all other routes, all nested layouts) so nothing but the root layout remained. Still hung.
    3. Built a completely fresh npx create-expo-app@latest --template blank-typescript (no expo-router at all, just a bare App.tsx), same Xcode/device. Still hung, identically — same last log line, same infinite wait.

    So on this device, the hang isn't specific to expo-router, to Tabs/NativeTabs, to nested navigators, or to any application code at all — it reproduces on the simplest possible RN app.

    Also ruled out

    • Precompiled vs. from-source RN Core: forced ios.buildReactNativeFromSource: true + EXPO_USE_PRECOMPILED_MODULES: false, full clean pod install (120 vs. 115 pods, confirmed no [Expo-precompiled] lines in the from-source install log). Hung identically either way.
    • Hermes bytecode integrity: compared the compiled main.jsbundle header bytes between a working iOS-simulator Release build and the hanging device build — identical magic (c61fbc03) and version bytes, so it's not a bytecode-version mismatch.
    • Hermes VM binary: confirmed arm64, valid codesign, satisfies its designated requirement on the device build.
    • Full device restart: no change, hangs identically on the next launch.
    • NativeTabs vs. classic Tabs: the production app normally uses expo-router/js-tabs. Switched both tab layouts to expo-router/unstable-native-tabs (NativeTabs) to test against the working row in the linked react-native#57511 census table — hung on 4/4 cold launches, no change.
    • The EXReactNativeFactory _appContext race fixed in Convert ReactTextInlineImageShadowNode #48576 — confirmed already present/applied in 57.0.20 (the local-variable pattern is in node_modules/expo/ios/AppDelegates/ExpoReactNativeFactory.mm), so that's not it either.
    • Missing / route (the root cause identified and fixed in Migrate SurfaceDelegate to Kotlin #47687) doesn't apply here — confirmed by the route-less reproduction above, and the production app's own route table does have a screen matching /.

    Takeaway

    Given a route-less, tab-less, effectively-empty app reproduces this with 100% consistency on this specific device (iPhone 13, iOS 26.6.1), the trigger condition may be broader than "nested navigator inside a Tabs tree" (per the census in react-native#57511) — possibly device/timing-class-dependent (older/slower A-series chip vs. the iPhone 17-class devices in the other reports?) rather than structure-dependent. Happy to run any additional instrumentation/diagnostics if useful — I have full console logs, build logs, and both precompiled and from-source Podfile.lock states saved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions