Repository navigation
Android removes and deletes every view of an unmounted subtree inside the frame that unmounts it #59448
Description
Description
When a subtree unmounts on Fabric Android, the frame that unmounts it runs one REMOVE and one DELETE mount instruction for every host view in the subtree. Only the root's removal changes what is on screen; its descendants leave the window with it, so their instructions cost the frame without changing a pixel.
The differ gives each removed child a DeleteMutation and a RemoveMutation and then diffs its subtree against no children (Differentiator.cpp, and the same in the reordering branch at L1264-L1284), so every descendant gets a pair of its own. SurfaceMountingManager executes each pair on the UI thread inside the batch: removeViewAt takes the view out of its parent ViewGroup (SurfaceMountingManager.kt), and deleteView destroys its state wrapper and event emitter and calls onDropViewInstance (SurfaceMountingManager.kt).
In the reproducer's release build on a Galaxy S23 (Android 14, 120 Hz, an 8.33 ms budget), unmounting a 241-view panel takes a 9.0–12.6 ms frame, of which REMOVE 241 is 4.8–6.8 ms and DELETE 241 1.8–2.6 ms.
RemoveDeleteTree (b6bbbf8, ca8481b) removed a subtree's root at once and deferred the rest of the teardown, behind a flag that was off in open source, and #45385 removed it as not worth its complexity. Here the per-view teardown is most of an over-budget frame. Would you take a change that removes the root inside the frame and runs its descendants' teardown after it, or is that the trade-off #45385 already decided?
Steps to reproduce
- Build the reproducer's app in release:
cd ReproducerApp && yarn install, thencd android && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a, and installapp/build/outputs/apk/release/app-release.apk. It mounts and unmounts a panel of 40 rows, 241 host views, once a second. - Record a 10 s Perfetto trace with the repository's
trace.cfg(thegfxandviewatrace categories and the app's own sections), starting the app after the trace, as its README shows. - On the app's main thread, every
Choreographer#doFramethat unmounts the panel holdsIntBufferBatchMountItem::mountInstructions::REMOVE numInstructions=241andIntBufferBatchMountItem::mountInstructions::DELETE numInstructions=241.
React Native Version
0.88.0
Affected Platforms
Runtime - Android
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
System:
OS: Windows 11 10.0.26300
CPU: "(32) x64 AMD Ryzen 9 9950X3D 16-Core Processor "
Memory: 172.51 GB / 255.67 GB
Binaries:
Node:
version: 26.12.1
path: C:\Program Files\nodejs\node.EXE
Yarn:
version: 1.22.22
path: ~\AppData\Roaming\npm\yarn.CMD
npm:
version: 12.2.1
path: C:\Program Files\nodejs\npm.CMD
Watchman: Not Found
SDKs:
Android SDK:
API Levels:
- "33"
- "35"
- "35"
- "35"
- "36"
- "37"
Build Tools:
- 35.0.0
- 36.0.0
- 36.1.0
- 37.0.0
System Images:
- android-26 | Google APIs Intel x86_64 Atom
- android-28 | Google APIs Intel x86_64 Atom
- android-29 | Google APIs Intel x86_64 Atom
- android-30 | Google APIs Intel x86_64 Atom
- android-31 | Google APIs Intel x86_64 Atom
- android-32 | Google APIs Intel x86_64 Atom
- android-33 | Google APIs Intel x86_64 Atom
- android-34 | Google APIs Intel x86_64 Atom
- android-35 | Google APIs Intel x86_64 Atom
- android-36.1 | Google Play Intel x86_64 Atom
- android-36.1 | Pre-Release 16 KB Page Size Google Play Intel x86_64 Atom
- android-37.0 | Google APIs Intel x86_64 Atom
Android NDK: Not Found
Windows SDK: Not Found
IDEs:
Android Studio: AI-262.9437.185.2621.16467767
Visual Studio: Not Found
Languages:
Java:
version: 17.0.20.1
path: C:\Program Files\Microsoft\jdk-17.0.20.101-hotspot\bin\javac.EXE
Ruby: Not Found
npmPackages:
"@react-native-community/cli":
installed: 20.2.0
wanted: 20.2.0
react:
installed: 19.3.0
wanted: 19.3.0
react-native:
installed: 0.88.0
wanted: 0.88.0
react-native-windows: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: Not found
newArchEnabled: Not found
Run in the reproducer's ReproducerApp.
Stacktrace or Logs
No crash. Galaxy S23, Android 14, 120 Hz; the reproducer's release build on 0.88.0,
the unmount frames of one 10 s trace, each with its mount-instruction batches:
+2118.6ms doFrame 9.04ms REMOVE 241 in 4.76ms · UPDATE_LAYOUT 2 in 0.06ms · UPDATE_OVERFLOW_INSET 2 in 0.05ms · DELETE 241 in 1.87ms
+4152.0ms doFrame 12.62ms REMOVE 241 in 6.82ms · UPDATE_LAYOUT 2 in 0.04ms · UPDATE_OVERFLOW_INSET 2 in 0.06ms · DELETE 241 in 2.40ms
+6179.8ms doFrame 11.29ms REMOVE 241 in 6.25ms · UPDATE_LAYOUT 2 in 0.05ms · UPDATE_OVERFLOW_INSET 2 in 0.04ms · DELETE 241 in 2.62ms
+8230.4ms doFrame 9.90ms REMOVE 241 in 5.54ms · UPDATE_LAYOUT 2 in 0.03ms · UPDATE_OVERFLOW_INSET 2 in 0.04ms · DELETE 241 in 1.81ms
The mount frames between them, with CREATE 241 in 54–59 ms, are what #59248 describes.
MANDATORY Reproducer
https://github.com/YevheniiKotyrlo/rn-fabric-subtree-unmount
Screenshots and Videos
No response
Activity
dave-otf commentedon Oct 25, 2026
My take: this warrants reopening a narrow experiment, but not assuming the old implementation should be restored unchanged. The reported unmount frames are 9.0–12.6 ms against an 8.33 ms budget; REMOVE alone is 4.8–6.8 ms and DELETE 1.8–2.6 ms for 241 views. Since REMOVE dominates, deferring only DELETE would miss most of this case. I would compare root detach plus deferred descendant REMOVE/DELETE against current batching, including LayoutAnimation, reattachment/key reuse, and the onDropViewInstance/event-emitter cleanup path. That would test whether the extra lifecycle complexity pays for this measured workload.