[Fabric] AnimatedPropsRegistry::syncedTags_ grows when views unmount while their animation is still running
Description
AnimatedPropsRegistry::syncedTags_ (Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp) keeps growing when animated views unmount while their animation is still running. FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS is on by default.
As far as we can tell, this is what happens:
- A component unmounts.
markNodeAsRemovableruns, thenhandleNodeRemovalscallsAnimatedPropsRegistry::removeTag, which removes the tag fromupdatesRegistry_,timestampMap_,syncedTags_andinvalidatedTags_. - The animation is still running. A spring lasts longer than the view, and detaching the style from
viewDescriptorstakes effect on the UI thread asynchronously. So the UI runtime still sends one or two_updatePropscalls for the removed tag. - Those updates put the tag back into
updatesRegistry_andtimestampMap_. The node is no longer marked as removable, so nothing removes it again. collectSettledUpdatestreats the entry as settled and adds the tag tosyncedTags_. On the next call the registry entry is evicted, butsyncedTags_is kept on purpose, so the tag of the unmounted view stays there for good.
AnimatedPropsRegistry does not override isEmpty(), so _registriesLeakCheck only checks updatesRegistry_ and reports ✅ even while syncedTags_ grows.
Expected: after the views unmount and their animations finish, syncedTags_ goes back to its baseline.
Actual: syncedTags_ grows with every mount/unmount cycle and never shrinks.
Measured on main (ad6c58a), Android Release build, with the repro screen below. The sizes come from a debug print added to _registriesLeakCheck:
| turn | registry | timestamps | syncedTags | invalidatedTags |
|---|---|---|---|---|
| start | 0 | 0 | 2 | 0 |
| 25 | 4 | 4 | 14 | 0 |
| 50 | 0 | 0 | 14 | 0 |
| 75 | 4 | 4 | 26 | 0 |
| 100 | 0 | 0 | 42 | 0 |
| 150 | 0 | 0 | 54 | 0 |
| 200 | 0 | 0 | 66 | 0 |
| 200 + 5 s idle | 0 | 0 | 66 | 0 |
The other registries reported by the leak check stay empty. The leak check still shows AnimatedPropsRegistry: ✅.
Possibly related: #9730, which covers a different case (an orphaned updatesRegistry_ entry when the last animated view unmounts and the GC interval stops). In this repro a static Animated.View stays mounted, so the GC interval keeps running and syncedTags_ still grows.
Steps to reproduce
- Check out the repro branch: https://github.com/finloop/react-native-reanimated/tree/repro/animated-props-registry-synced-tags-leak. It is current
mainplus one commit: theSyncedTagsLeakExamplescreen and a line in_registriesLeakCheckthat prints theAnimatedPropsRegistrycontainer sizes. yarn, then buildapps/fabric-examplefor Android in Release (./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a) and install it.- Open Reanimated examples → syncedTags_ leak repro and press Start.
- Each turn mounts 4 views animated with a slow
withSpring, unmounts them after 450 ms, and waits 150 ms. Every 25 turns and 5 s after turn 200, the screen and logcat ([SyncedTagsLeak]) show the_registriesLeakCheckoutput. WatchsyncedTagsgrow.
The core of the screen:
function SpringBox({ index }: { index: number }) {
const progress = useSharedValue(0);
useEffect(() => {
// slow spring: still running when the view unmounts after 450 ms
progress.value = withSpring(1, { stiffness: 20, damping: 4 });
}, [progress]);
const style = useAnimatedStyle(() => ({
opacity: 0.3 + 0.7 * progress.value,
transform: [{ translateX: index * 50 * progress.value }],
}));
return <Animated.View style={[styles.box, style]} />;
}
// Turn: mount 4 <SpringBox key={`${turn}-${i}`} />, unmount after 450 ms, pause 150 ms, x200.
// A static <Animated.View /> stays mounted the whole time.Snack or a link to a repository
Reanimated version
4.7.0-main (main @ ad6c58a)
Worklets version
0.13.0-main
React Native version
0.87.0
Platforms
Android
JavaScript runtime
Hermes
Workflow
React Native CLI
Architecture
New Architecture (Fabric renderer)
Reanimated feature flags
No
React Native release level
Stable
Build type
Release app & production bundle
Device
Android emulator
Host machine
macOS
Device model
Pixel 10 emulator (sdk_gphone16k_arm64), Android 17 (API 37)
Acknowledgements
Yes
Source: software-mansion/react-native-reanimated