Description
Components that hold a useSharedValue and drive an Animated.View style through useAnimatedStyle keep their Fabric shadow subtree alive after they unmount. In an app with such a component in every list row, every visit to the screen adds the rows' shadow trees to the native heap and nothing releases them until the process dies. We first measured this on 4.5.1 (about 25 MB per visit with roughly forty rows), replaced the component with React Native's Animated API as a workaround, and have now re-measured the original component against 4.5.5 / worklets 0.10.4 and 4.6.0 / worklets 0.12.2: the retention is unchanged.
Steps to reproduce
- A pressable row component:
const AnimatedPressableBase = Animated.createAnimatedComponent(Pressable);
export function AnimatedPressable({ children, style, pressScale = 0.97, pressOpacity = 0.85, onPressIn, onPressOut, ...rest }) {
const pressed = useSharedValue(0);
const animStyle = useAnimatedStyle(() => ({
transform: [{ scale: 1 - (1 - pressScale) * pressed.value }],
opacity: 1 - (1 - pressOpacity) * pressed.value,
}));
return (
<AnimatedPressableBase
{...rest}
onPressIn={(e) => { pressed.value = withSpring(1); onPressIn?.(e); }}
onPressOut={(e) => { pressed.value = withSpring(0); onPressOut?.(e); }}
style={[style, animStyle]}
>
{children}
</AnimatedPressableBase>
);
}
- Render it as the row of a list screen (a
FlashList with about ten visible rows plus a few cards in a header is enough) behind a tab or a route that unmounts and remounts on navigation.
- On a Release build on a physical Android device, navigate Home → list screen → Home repeatedly by deep link and read the native heap after each round trip:
adb shell dumpsys meminfo <pid> | grep 'Native Heap' # HeapAlloc column
We drive this with a script: cold start on Home, then am start -a android.intent.action.VIEW -d "<scheme>:///<route>" alternating Home and the list route, five seconds settle, dumpsys meminfo per round trip, and compare the floor of the last three round trips against the first.
Expected
The native heap plateaus after the first visit (the fixed component using react-native's Animated API with the native driver plateaus at +4 to 5 MB over six round trips on the same screen).
Actual
Native HeapAlloc (dumpsys meminfo, KB / 1024) after each Home ↔ list round trip, Pixel 8a, Release build, React Native 0.86.3, Expo SDK 57, new architecture, same JS code in all three runs (only the Reanimated/worklets pair differs):
| Reanimated / worklets |
RT1 |
RT2 |
RT3 |
RT4 |
RT5 |
RT6 |
floor(RT4–6) − RT1 |
| 4.5.1 / 0.10.1 |
86.3 MB |
103.5 |
107.5 |
102.4 |
114.5 |
105.7 |
+16.1 MB |
| 4.5.5 / 0.10.4 |
86.7 MB |
104.0 |
109.3 |
114.3 |
118.6 |
119.4 |
+27.6 MB |
| 4.6.0 / 0.12.2 |
86.2 MB |
98.1 |
99.4 |
104.3 |
110.0 |
111.5 |
+18.1 MB |
Same screen, rows on RN Animated (no Reanimated in rows), 4.5.1 |
81.8 MB |
97.8 |
92.1 |
86.3 |
96.3 |
— |
+4.5 MB (5 round trips) |
Allocation attribution on 4.5.1 (malloc-debug wrap.sh with backtrace and record_allocs, size-band backtraces) pointed at Fabric ShadowNode instances retained through the Reanimated animated-props registry after the owning views were unmounted.
Platform
Android (Pixel 8a, Android 16), React Native 0.86.3, Expo SDK 57.0.20, new architecture, Hermes. iOS shows the same growth pattern (Jetsam terminations after roughly thirty visits on an iPhone 13 Pro with the 4.5.1 build before the workaround).
Workaround
Move per-row press feedback to react-native's Animated API with useNativeDriver: true; the retention disappears (last row of the table).
Description
Components that hold a
useSharedValueand drive anAnimated.Viewstyle throughuseAnimatedStylekeep their Fabric shadow subtree alive after they unmount. In an app with such a component in every list row, every visit to the screen adds the rows' shadow trees to the native heap and nothing releases them until the process dies. We first measured this on 4.5.1 (about 25 MB per visit with roughly forty rows), replaced the component with React Native'sAnimatedAPI as a workaround, and have now re-measured the original component against 4.5.5 / worklets 0.10.4 and 4.6.0 / worklets 0.12.2: the retention is unchanged.Steps to reproduce
FlashListwith about ten visible rows plus a few cards in a header is enough) behind a tab or a route that unmounts and remounts on navigation.We drive this with a script: cold start on Home, then
am start -a android.intent.action.VIEW -d "<scheme>:///<route>"alternating Home and the list route, five seconds settle,dumpsys meminfoper round trip, and compare the floor of the last three round trips against the first.Expected
The native heap plateaus after the first visit (the fixed component using
react-native'sAnimatedAPI with the native driver plateaus at +4 to 5 MB over six round trips on the same screen).Actual
Native
HeapAlloc(dumpsys meminfo, KB / 1024) after each Home ↔ list round trip, Pixel 8a, Release build, React Native 0.86.3, Expo SDK 57, new architecture, same JS code in all three runs (only the Reanimated/worklets pair differs):Animated(no Reanimated in rows), 4.5.1Allocation attribution on 4.5.1 (malloc-debug
wrap.shwithbacktraceandrecord_allocs, size-band backtraces) pointed at FabricShadowNodeinstances retained through the Reanimated animated-props registry after the owning views were unmounted.Platform
Android (Pixel 8a, Android 16), React Native 0.86.3, Expo SDK 57.0.20, new architecture, Hermes. iOS shows the same growth pattern (Jetsam terminations after roughly thirty visits on an iPhone 13 Pro with the 4.5.1 build before the workaround).
Workaround
Move per-row press feedback to
react-native'sAnimatedAPI withuseNativeDriver: true; the retention disappears (last row of the table).