software-mansion / software-mansion/react-native-screens

[New Arch] react-native-screens Fragment accumulation bloats onSaveInstanceState → TransactionTooLargeException on activityStopped

Open
#4,487 3 comments 0 reactions 1 assignee View on GitHub

@t0maboro is already working on this.

Since Aug 12, 2026.

platform:android repro-provided
Dominant language
TypeScript
Stars
3.7k
Forks
713
Avg merge
2d 23h
Merged PRs (30d)
71

Description

Description

Under the New Architecture (Fabric), react-native-screens mounts one native Fragment per pushed screen (ScreenStackFragment, each with a nested childFragmentManager/ScreenFragment). On a deep push-navigation stack these accumulate in the host Activity's saved state. When the OS stops the Activity (app backgrounded), onSaveInstanceState serialises all of them, and in a real app the parcel exceeds the Binder transaction budget → fatal, unhandled android.os.TransactionTooLargeException on activityStopped.

This is a save-time crash, distinct from the well-known restore-time crashes (#17, #3317) that AutoRemovingFragment / RNScreensFragmentFactory / super.onCreate(null) address. Those do not help here: the oversized parcel is rejected by the Binder at save time, before any restore.

Production crash evidence. Real TooLargeTool-style breakdown from a shipping app on this stack (the parcel that crashed at activityStopped, ~639 KB):

androidx.lifecycle.BundlableSavedStateRegistry.key [637 KB]
    android:support:activity-result [208 KB]
    android:support:fragments      [428 KB]   <- ~150 ScreenStackFragment/ScreenFragment entries
        fragment_<uuid> [2868]
            childFragmentManager [2176]
                fragment_<uuid> [1752]
                    childFragmentManager [1068]
        ... (x150)
  • Device: Samsung Galaxy A57 (SM-A576B), Android 16, 7.8 GB RAM — not low-memory.
  • Each real fragment contributes ~2.8 KB (FragmentManager metadata + the nested childFragmentManager); at 150+ retained fragments this is 428 KB.

About the reproduction (please read). The linked repo demonstrates the accumulation mechanism — it auto-pushes a native-stack screen and logs, on background, the saved-state parcel size and that android:support:fragments is present (regHasFragments=true). It does not, by itself, cross the ~1 MB limit and crash, and that is worth stating plainly: with trivial screens the saved state is only ~75 bytes/fragment (measured on API 34), so a toy stack stays far under the limit. The production crash is emergent from real screens × deep navigation (real screens save ~2.8 KB/fragment; long sessions reach 150+ retained fragments). The repo shows the code path and lets you watch the parcel grow with depth; the crash-scale evidence is the production bundle above.

Why this seems in scope for react-native-screens. RNS already declares its fragments non-restorable — it discards restored fragment state and rebuilds from JS (AutoRemovingFragment, and the discussion in #96). But it still lets that state be saved into the Activity parcel, where on deep stacks it triggers TransactionTooLargeException. Since RNS never consumes the saved fragment state, arguably it should not let it balloon the save parcel — e.g. opt its ScreenStackFragments out of instance-state saving, or expose a supported way to bound it.

App-side workaround (and a gotcha). Stripping the fragment state in MainActivity.onSaveInstanceState stops the crash — RNS rebuilds fine from JS. Note: on androidx.fragment 1.8.x the FragmentManager state is saved via SavedStateRegistry, so it is nested — the classic top-level outState.remove("android:support:fragments") is a no-op (measured: 0 bytes removed). It must be reached inside the registry bundle:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.getBundle("androidx.lifecycle.BundlableSavedStateRegistry.key")
        ?.remove("android:support:fragments")
}

This depends on internal androidx key strings, which is why a first-party RNS option would be preferable.

Steps to reproduce

Reproduction repo: https://github.com/MisterMunchkin/rns-4487-repro

  1. git clone https://github.com/MisterMunchkin/rns-4487-repro && cd rns-4487-repro && npm install
  2. npx expo prebuild --platform android --clean then npx expo run:android on an Android 13+ emulator/device. New Architecture is enabled via app.json; a parcel-size logger is injected into MainActivity by plugins/with-parcel-logger.js.
  3. In a second terminal: adb logcat -s RNSRepro.
  4. The app auto-pushes a self-referential native-stack screen to TARGET_DEPTH. When it settles, enable Developer Options → "Don't keep activities" (forces the save+stop deterministically on an emulator; production hits it via natural memory pressure), then press Home.
  5. RNSRepro logs onSaveInstanceState parcel=<N> bytes | regHasFragments=true — confirming the react-native-screens fragments accumulate in the Activity's saved state (nested under androidx.lifecycle.BundlableSavedStateRegistry.key). Raise TARGET_DEPTH (or run against content-heavy screens) to watch the parcel climb toward the Binder limit; at production scale (real screens, 150+ fragments ≈ 428 KB) it crosses ~639 KB and throws TransactionTooLargeException at activityStopped.
Snack or a link to a repository

https://github.com/MisterMunchkin/rns-4487-repro

Screens version

4.18.0

React Native version

0.81.5

Platforms

Android

JavaScript runtime

Hermes

Workflow

Expo bare workflow (expo prebuild / CNG; androidx.fragment 1.8.9)

Device

Real device (production crash) + Android emulator (mechanism)

Device model

Samsung Galaxy A57 (SM-A576B), Android 16

Acknowledgements

Yes

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.