[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
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 5/5
- 預估耗時
- 一週以上
- 新手友好度
- 25/100
- Issue 類型
- 缺陷
- 描述清晰度
- 需要釐清
- 活躍度
- 活躍
- 技術堆疊
- cpp, ios, react-native
- 領域
- frontend, mobile, performance
研究方向
從必要的 rn086-tabs-stack-hang-repro 儲存庫開始,執行其文件記載的 Release 建置以及 tools/sim-census.sh;確認 boot-js-start.txt 和 boot-heartbeat.txt 持續推進,同時 boot-mounted.txt 仍然缺失。將可重現的 Tabs 設定與標準 NativeTabs 範本進行比較,接著追蹤 thread dump 所涉及的 React Native 0.86 Fabric 和 bridgeless 啟動路徑。完成標準是 Release 應用程式能可靠地掛載,且 census 在沒有錯誤的情況下記錄 boot-mounted.txt。
由索引模型根據 Issue 內容生成。
描述
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-
Tabsapp whose tab screens each host a nested native Stack (react-native-screens4.25.2via expo-router57.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
Tabsnavigator (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 stockcreate-expo-appdefault template (which usesNativeTabs) 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
ErrorUtilsglobal handler, aconsole.errorwrap, and aglobalThis.reportErrorwrap all stay silent for the entire hang. - The JS event loop stays alive. A
setTimeoutheartbeat 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_msgrunloop wait, andcom.facebook.react.runtime.JavaScriptis likewise parked inmach_msgrunloop 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):
- Clone the reproducer: https://github.com/sjconnolly/rn086-tabs-stack-hang-repro (stock
create-expo-appdefault template for Expo SDK 57 / RN 0.86.0, restructured to 3Tabstabs that each host a nested native Stack, plus boot-marker instrumentation) npm install && npx expo prebuild -p iosxcodebuild -workspace ios/rn086tabsstackhangrepro.xcworkspace -scheme rn086tabsstackhangrepro -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath build-sim buildtools/sim-census.sh <sim-udid> <path-to-built-.app> 10- installs, cold-launches 10×, classifies each run from the marker files- Observe: native splash forever on every launch;
boot-js-start.txtwritten andboot-heartbeat.txtadvancing every second for as long as the app is left running;boot-mounted.txtnever written;boot-error.txtnever written
Device path (mirrors how this was found; needs a signing team in app.json → ios.appleTeamId):
- Build the same workspace with
-destination 'id=<device-udid>', install withxcrun devicectl device install app, thentools/launch-census.sh <device-udid> 10 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- 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.
- 主要語言
- C++
- 星號
- 127k
- 分支
- 25.3k
- PR 合併指標
- 30 天內沒有已合併 PR
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
react/react-native 的其他 Issue
-
難度 2/5 1-3 小時 新手友好度 78/100
react/react-native#59006 ·
維護者通常 1 天內回覆
-
Needs: Author Feedback Needs: Repro
難度 2/5 1-3 小時 新手友好度 76/100
react/react-native#58952 · 1 則留言 ·
維護者通常 1 天內回覆
-
Needs: Attention Needs: Repro
難度 2/5 1-3 小時 新手友好度 78/100
react/react-native#58904 · 2 則留言 ·
維護者通常 1 天內回覆
-
Needs: Author Feedback Needs: Repro
難度 2/5 1-3 小時 新手友好度 75/100
react/react-native#58659 · 2 則留言 ·
維護者通常 1 天內回覆
-
Needs: Author Feedback Needs: Repro
難度 1/5 1 小時以內 新手友好度 92/100
react/react-native#58621 · 1 則留言 ·
維護者通常 1 天內回覆
查看 react/react-native 的全部 Issue
相似的 Issue
-
bug format I/O medium priority rt_lib
難度 2/5 1-3 小時 新手友好度 72/100
維護者通常 1 天內回覆
-
refactor
難度 2/5 1-3 小時 新手友好度 78/100
維護者通常 1 天內回覆
-
bug
難度 2/5 1-3 小時 新手友好度 62/100
compiler-explorer/compiler-explorer#9272 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 65/100
-
難度 1/5 1 小時以內 新手友好度 76/100
維護者通常 1 天內回覆