react-native-reanimated: Memory leak when updating stateful value in component
Description
After implementing a swipe gesture on list items inspired by SwipeableListExample.js
I noticed a memory leak in my app. I’ve been able to reproduce with minimal changes to SwipeableListExample.js
in Reanimated example project.
It seems that calling a function declared in the same component as the worklet calling it, is creating a memory leak.
Maybe I’m misusing the library but I haven’t found any reference to that in the documentation so I’m assuming this is a bug.
Steps To Reproduce
- Open
SwipeableListExample.js
in the Example project - Declare a stateful value with
useState()
in theListItem
component. - Change the stateful value in the
useAnimatedGestureHandler
worklet. - Take an initial memory snapshot using Safari
- Run project and navigate multiple times between home and the Swipeable List example
- Take a second memory snapshot using Safari and look for the number of PanGestureHandler
Expected behavior
Previous PanGestureHandler
components and their children should be deallocated when their parent is deallocated or no longer attached to any parent. This is the behavior of the example project without modifications.
Actual behavior
All PanGestureHandler
components and their children stay in memory until application is killed.
Screenshots
Safari Dev Tools, list of new PanGestureHandler
in memory since initial snapshot.
Detail of one retained PanGestureHandler
Snack or minimal code example
Edited part of SwipeableList.js
(see comments):
function ListItem({ item, onRemove }) {
const isRemoving = useSharedValue(false);
const translateX = useSharedValue(0);
// Declare stateful value
const [isSwiping, setIsSwiping] = useState(false);
const handler = useAnimatedGestureHandler({
onStart: (evt, ctx) => {
ctx.startX = translateX.value;
// Modify stateful value from worklet
runOnJS(setIsSwiping)(true);
},
onActive: (evt, ctx) => {
const nextTranslate = evt.translationX + ctx.startX;
translateX.value = Math.min(0, Math.max(nextTranslate, MAX_TRANSLATE));
},
onEnd: (evt) => {
if (evt.velocityX < -20) {
translateX.value = withSpring(
MAX_TRANSLATE,
springConfig(evt.velocityX)
);
} else {
translateX.value = withSpring(0, springConfig(evt.velocityX));
}
},
});
// ... Rest of ListItem implementation. Truncated for clarity
}
I can provide a project on Github if it helps.
Package versions
- React: 16.13.1
- React Native: 0.63.0
- React Native Reanimated: Master @ 29491efc1851df320c2fa1ba90b5c217bd7a22cc (Nov 30th)
- react-native-gesture-handler: 1.8.0
- NodeJS: 12.16.1
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 5
- Comments: 16 (9 by maintainers)
🏓 Any updates or workaround for this? It seems like the cause of leak is different after latest improvements. Maybe open a separate issue for discussing it? Having
useAnimatedGestureHandler
in a flatlist with a few timing worklets makes the leak a big issue.Just want to note that you guys are doing amazing work here 💪 Would totally look into this myself, but don’t have the cpp experience.
Today I discovered that it could just be a problem of the gesture handler itself. Such code leaks memory:
It has nothing to do with reanimated.
We will let you know when we check the GH.
Sorry, I should have been clearer, forgot my previous message.
Yes, you’re right @karol-bisztyga,
useState
doesn’t play a direct role. I simply meant that usingsetState
in the worklet is what causes the leak.Here is another example similar to the first one (without any use of any state this time 😅):
If at any point in a worklet declared in the
ListItem
function you callrunOnJS(completeSwipe)();
, it creates some kind of reference cycle and thePanGestureHandlers
and their children leak.I also tried your code and while memory usage is increasing it looks like a different issue because the
PanGestureHandlers
are correctly deallocated in that case.Let me know if it’s still unclear. I can provide a sample project if needed. Thank you!
Sure, no problem, I just didn’t understand 😉
Here is a minimal code example I found:
I think
useState
creates a problem here. Actually, capturingsetState
function does as it has ref to FiberNode and as a result to the PanGestureHandler what creates a reference loop.