Skip to content

Fabric shadow subtrees of unmounted components using useSharedValue/useAnimatedStyle stay retained (native heap grows per screen visit) — reproduced on 4.5.1, 4.5.5 and 4.6.0 #10521

Description

@fabianpy97

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

  1. 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>
  );
}
  1. 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.
  2. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Missing infoThe user didn't precise the problem enoughMissing reproThis issue need minimum repro scenario

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions