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

[Android][Fabric] Screen.fragmentWrapper retains destroyed ScreenFragment via Fabric's mTagToViewState

Open
#3,755 14 comments 1 reaction 1 assignee View on GitHub

@t0maboro is already working on this.

Since Mar 12, 2026.

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

Description

Description

On Android with Fabric (New Architecture), destroyed ScreenStackFragment / ScreenFragment instances are retained in memory because Screen.fragmentWrapper is never nulled after the fragment receives onDestroy().

Fabric's SurfaceMountingManager.mTagToViewState keeps Screen and ScreenContainer views alive as long as their surface is mounted — this is normal Fabric behavior. But because Screen.fragmentWrapper still points to the destroyed fragment, that fragment (and everything it retains) can never be GC'd.

This is a separate leak path from ScreenDummyLayoutHelper (#3636), but both stem from the same root cause: references not being cleaned up after Activity/Fragment destruction.

Leak traces (LeakCanary 2.14)

Trace 1 — ScreenContainer.parentScreenWrapperScreenStackFragment

┬───
│ GC Root: Global variable in native code
│
├─ com.facebook.react.fabric.FabricUIManager instance
│    Leaking: NO (ScreenContainer↓ is not leaking)
│    ↓ FabricUIManager.mMountingManager
├─ com.facebook.react.fabric.mounting.MountingManager instance
│    ↓ MountingManager.mostRecentSurfaceMountingManager
├─ com.facebook.react.fabric.mounting.SurfaceMountingManager instance
│    ↓ SurfaceMountingManager.mTagToViewState
├─ java.util.concurrent.ConcurrentHashMap instance
├─ SurfaceMountingManager$ViewState instance
│    ↓ SurfaceMountingManager$ViewState.mView
├─ com.swmansion.rnscreens.ScreenContainer instance
│    Leaking: NO (View attached)
│    ↓ ScreenContainer.parentScreenWrapper
│                      ~~~~~~~~~~~~~~~~~~~
╰→ com.swmansion.rnscreens.ScreenStackFragment instance
     Leaking: YES (Fragment#onDestroy() callback received)
     Retaining 941 B in 27 objects

Trace 2 — Screen.fragmentWrapperScreenFragment

┬───
│ GC Root: Global variable in native code
│
├─ com.facebook.react.fabric.FabricUIManager instance
│    ↓ FabricUIManager.mMountingManager
├─ MountingManager instance
│    ↓ MountingManager.mostRecentSurfaceMountingManager
├─ SurfaceMountingManager instance
│    ↓ SurfaceMountingManager.mTagToViewState
├─ ConcurrentHashMap instance
├─ SurfaceMountingManager$ViewState instance
│    ↓ SurfaceMountingManager$ViewState.mView
├─ com.swmansion.rnscreens.Screen instance
│    Leaking: NO (View attached)
│    ↓ Screen.fragmentWrapper
│             ~~~~~~~~~~~~~~~
╰→ com.swmansion.rnscreens.ScreenFragment instance
     Leaking: YES (Fragment#onDestroy() callback received)
     Retaining 855 B in 26 objects
Root cause

Screen.fragmentWrapper (Screen.kt#L57) is set when a fragment is created but never nulled on fragment destruction.

Meanwhile, ScreenContainer.parentScreenWrapper is properly nulled in onDetachedFromWindow() (ScreenContainer.kt#L290), showing the intended cleanup pattern already exists in the codebase — it was just not applied to Screen.fragmentWrapper.

Field Nulled on cleanup?
ScreenContainer.parentScreenWrapper Yes (onDetachedFromWindow)
Screen.fragmentWrapper No — never nulled
Suggested fix

Null out screen.fragmentWrapper in ScreenFragment.onDestroy():

// ScreenFragment.kt — onDestroy()
override fun onDestroy() {
    super.onDestroy()
    // ... existing code ...
    childScreenContainers.clear()
    screen.fragmentWrapper = null  // break the retention chain
}
Environment
  • react-native-screens: 4.20.0 (also confirmed unpatched on main as of 4.24.0)
  • React Native: 0.81 (Fabric / New Architecture enabled)
  • Android: API 29 (Samsung Galaxy A7)
  • LeakCanary: 2.14

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.