Closed mrousavy closed 3 years ago
Also, are there fundamental differences between useAnimatedStyle
and useDerivedValue
in terms of dealing with results that are shallow-equal? E.g.: When a useDerivedValue
executes and it's result stays the same, other useDerivedValue
s and useAnimatedStyle
s do not re-run since the input is the same (afaik).
Is this also the case for useAnimatedStyle
? Does it forcefully re-trigger a native re-render/re-draw even if the output' style (return value of useAnimatedStyle
) is still the same?
Actually, I've just tried to disable random parts of the animation and noticed that the scale
transform:
{ scale: withTiming(isSwipeMode ? (isCurrentHeader ? 1.2 : 1) : 1, { duration: 150 })
in the useAnimatedStyle
hook is causing a lot of performance issues, even though the inputs haven't changed. I'm trying to run scale animations imeratively using useEffect
hooks now, that works a bit better. It's still under 30 FPS.
Hello! Congratulations! Your issue passed the validator! Thank you!
It seems to be related to the performance issues I had and we talked about @karol-bisztyga
Basically, it subscribes for too many mappers (animated style callback) and for some reason it had hard time executing all of them during the single frame update
Hello! First of all thanks to @mrousavy for posting such a well-prepared issue! You must've taken your time to make it look like this, I really appreciate that π !
I got a little bit confused about whether your components are inside ScrollView
or PanGestureHandler
that tries to imitate a ScrollView
(you started the post by mentioning the first one but in repro, there is the other one). Could you clarify? Still, both options should work smoothly but just wanted to understand it better. The code snippet you posted(Header.tsx
- I understand it's rendered multiple times in some kind of loop(Array.map
in a parent component, something like that?), right?). Maybe it's just me and I don't get it right, then I'm sorry.
I'll be looking into it, I wouldn't be surprised if it turned out that there are several distinct problems which together cause those big lags.
Also, are there fundamental differences between
useAnimatedStyle
anduseDerivedValue
in terms of dealing with results that are shallow-equal? E.g.: When auseDerivedValue
executes and it's result stays the same, otheruseDerivedValue
s anduseAnimatedStyle
s do not re-run since the input is the same (afaik). Is this also the case foruseAnimatedStyle
? Does it forcefully re-trigger a native re-render/re-draw even if the output' style (return value ofuseAnimatedStyle
) is still the same?
I think it should work that way, however, it doesn't(I've reproduced such a case already) and that could be the first problem here.
@karol-bisztyga Thanks for responding.
<PanGestureHandler
onGestureEvent={onGestureEvent}
maxPointers={1}
activeOffsetX={GESTURE_HANDLER_RANGE}
failOffsetY={GESTURE_HANDLER_FAIL_RANGE}>
<Reanimated.View style={styles.sliderContainer}> {/* <-- That's just width: SCREEN_WIDTH, height: SCREEN_HEIGHT */}
<Reanimated.View style={[styles.slider, sliderStyle, sliderAnimatedStyle]}> {/* <-- That's flexDirection: 'row', width: SCREEN_WIDTH * headers.length, transform: [{ translateX: translateX.value }] */}
{swipers}
{headers} {/* Those are the individual headers, memoized so I can return null ("unmount them") for those that aren't in the viewport, story headers have a custom style of position: absolute and their left: inset. Also, they interpolate their own translateX on the translateX value from the PanGestureHandler parent, see the code in my original issue/post */}
</Reanimated.View>
</Reanimated.View>
</PanGestureHandler>
const headers = useMemo(
() =>
stories.map((story, i) => {
if (Math.abs(index - i) > 1) {
return null; // it's out of viewport bounds
} else {
return (
<StoryHeader
key={`${story.id}.header`}
style={styles.storyHeader}
story={story}
indexInSwiper={i}
swiperTranslateX={translateX}
onPress={() => onHeaderPressed(i)}
/>
);
}
}),
[index, isExperimental, isSwipeMode, onHeaderPressed, stories, translateX],
);
If you need any more details, please let me know! Thanks for looking into this π€
Ok so there are two major problems here:
const sv = useSharedValue(50)
const uas = useAnimatedStyle(() => {
return { width: sv.value }
})
for (let i = 0; i < 1000; ++i) {
sv.value = 100;
}
In that case, the mapper inside of the uas is going to be triggered 1000 times(plus initial updater run but I mean 1000 extra times that shouldn't happen).
As for this problem, I'm going to push a solution soon.
In the meantime will also hunt for more issues here(maybe there are some).
Extrapolate.clamp
for my translateX
interpolation, and if the view is out of bounds, the header translateX value will get clamped, so it doesn't re-run the animated style hook (since the inputs are the same).I've done some testing and my fix in #1502 seems to reduce 90%+ of the lag. BUT there has to be a proper approach applied. The main idea is to have that one useDerivedValue
per item in the render loop(Array.map
) which would be some kind of 'entry point' for all the other mappers for that item. Then every other worklet there would depend on that value. Something like this(pseudo-code):
items.map((item) => {
const udv = useDerivedValue(() => {
// ... calculate something
return ... // e.g. null/undefined when the item wouldn't be visible, original number when it would
})
const st1 = useAnimatedStyle(() => {
return { transform: [{ translateX: udv.value === null ? -100 : udv.value }] }
})
const st2 = useAnimatedStyle(() => {
return { scale: withTiming(udv.value === null ? 1 : 0.7) }
})
return <...>
})
I wouldn't treat it as some kind of workaround but rather a good practice. This happens only when there is a lot of mappers spawned which recalculate a lot. Maybe there's some way we could improve the process more on the reanimated 2 side but keep in mind that the application's logic matters a lot too.
Below I'm pasting the code that I used for testing/reproducing. It's a standalone component so just feel free to paste it and launch it.
I've run it on iPhone 11 and without the fix from #1502 it would drop to extremely low FPS values(both JS and UI, often <10, sometimes even 0, lol) but with the fix applied it stays at 60FPS(both) and drops to 50+ when there are more components involved(meaning when you swipe so the new ones appear)(also, if you remove console.log
's that little drop only appears on UI, on JS it remains at 60). I realize that's still a frame drop there and I will look deeper, but just wanted to point out a direction for solving the problem(also the calculations in this particular example could be not very precise as I didn't pay that much attention to this).
@mrousavy what do you think? I'm probably going to try to also refactor your code a little bit in such a manner.
That's awesome! That's essentially what I was referring to with "clamping" the outputs, so they stay the same and won't re-trigger dependant worklets (useAnimatedStyle
). I'll test that out in my app sometime this week to see how big of an impact it's going to have, but it looks promising. Thanks again for looking into this.
Great! Looking forward to hearing about the results, thx! π
I'm also seeing huge performance issues when using this useAnimatedStyles
within FlatList items (about ~40 list items are rendered on the screen at once).
const animatedStyles = {
styleAnimated: useAnimatedStyle(() => {
const rotate = interpolate(
shared.translation.x.value,
[-windowDimensions.width * 0.95, 0, windowDimensions.width * 0.95],
[-10, 0, 10]
) as unknown as string;
const opacity = interpolate(
shared.translation.x.value,
[-(windowDimensions.width / 3), 0, windowDimensions.width / 3],
[1, 0.45, 1]
);
return ({
transform: [
{
translateX: shared.translation.x.value
},
{
rotate: `${rotate}deg`
}
],
backgroundColor: (!shared.didSnapBackAnimationFinish.value ? !shared.isDragging.value : shared.translation.x.value === 0) ? 'transparent' : (shared.translation.x.value < 0 ? `rgba(0,0,0,${opacity})` : `rgba(0,64,159,${+opacity})`)
});
})
The app runs very slow (<30fps), but when I comment out this useAnimatedStyle within the list item, it starts running smoothly again at 60fps. Is there any workaround to improve performance when using rendering many reanimated hooks?
Is there any workaround to improve performance when using rendering many reanimated hooks?
I played around with performance issues A LOT, and came to the following conclusion:
useAnimatedStyle
for multiple views. there is a PR open to support this here: #1470 ). Unfortunately this does not work in all cases, since sometimes we also have to add some sort of parallax and interpolate each view differently. (e.g. my original issue, and probably your's too.)as for point 4.1, @piaskowyk is doing some interesting work over at https://github.com/software-mansion/react-native-reanimated/pull/1879 ^^
Description
I've created a view which is essentially a horizontal ScrollView that can only be swiped one item at a time. Each item (= category) has a header showing the category's name. To somehow reveal to the user what the next page/previous page has to offer, I decided to show a tiny bit of the headers flowing into the screen.
Demo
While you can't really see the difference in a compressed, 320 pixel wide, 25 FPS GIF, it is extremely noticeable in the app, since you can swipe/scroll normally without the parallax, and once you enable the header parallax it looks like a powerpoint presentation. I'd say without parallax it's around 60 FPS, with parallax about 5 FPS.
Attempts to solve
I've tried multiple approaches to solve those performance issues, here's what I tried:
useDerivedValues
into a singleuseAnimatedStyle
) so that there's a few less worklets executing at once - didn't make a difference at all.removeClippedSubviews
property on the container to remove all views that are offscreen, so not rendering them if they're overflowing. That requiresoverflow: 'hidden'
to be set, which I did, so now only everything that's on screen should be rendered. This doesn't affect worklets though, since still every singleuseDerivedValue
anduseAnimatedStyle
worklet is executing per frame on drag. For 30 Stories, that's 1useAnimatedStyle
and 1useAnimatedGestureHandler
for the "list" container, and 30useDerivedValue
s and 30useAnimatedStyle
s for the Headers. Can we ignore them if RCTView removes that view due to clipping?Steps To Reproduce
<PanGestureHandler>
and translate those views using thetranslateX
(plus someoffsetX
)translateX
value and it's index in the SwiperuseAnimatedStyle
which interpolates the current Swiper'stranslateX
to parallax-like translate the header towards the screen corners so they get revealed.Expected behavior
I expect it to run smoothly
Actual behavior
It runs terribly stuttery.
Snack or minimal code example
```tsx function StoryHeader({ story, style: _style, indexInSwiper, swiperTranslateX, isSwipeMode, onPress, ...passThroughProps }: StoryHeaderProps): React.ReactElement { const [width, setWidth] = useState(0); const onLayout = useCallback( ({ nativeEvent: { layout: { width: newWidth }, }, }: LayoutChangeEvent) => { setWidth(newWidth); }, [setWidth], ); const thisTranslateX = useMemo(() => -(SCREEN_WIDTH * indexInSwiper), [indexInSwiper]); const offsetX = useDerivedValue(() => { if (isSwipeMode) { return withTiming(width / 2, { duration: 200, easing: Easing.back(1), }); } else { return withTiming(width / 2 - HEADER_PREVIEW_OVERFLOW, { duration: 300, easing: Easing.elastic(1), }); } }, [isSwipeMode, width]); const animatedStyle = useAnimatedStyle(() => { const translateX = interpolate( swiperTranslateX.value, [ thisTranslateX - SCREEN_WIDTH - SCREEN_WIDTH, thisTranslateX - SCREEN_WIDTH, thisTranslateX, // thisTranslateX + SCREEN_WIDTH, thisTranslateX + SCREEN_WIDTH + SCREEN_WIDTH, ], [ SCREEN_WIDTH / 2, SCREEN_WIDTH / 2 - offsetX.value, // Swiper is at Next slide 0, // Swiper is at current slide -(SCREEN_WIDTH / 2) + offsetX.value, // Swiper is at Previous slide -(SCREEN_WIDTH / 2), ], Extrapolate.CLAMP, ); const isCurrentHeader = between(translateX, -10, 10); return { // opacity: withTiming(isSwipeMode ? (isCurrentHeader ? 1 : 0) : 1, { duration: 300 }), opacity: 1, transform: [{ translateX: translateX }, { scale: withTiming(isSwipeMode ? (isCurrentHeader ? 1.2 : 1) : 1, { duration: 150 }) }], }; }, [isSwipeMode, offsetX, swiperTranslateX, thisTranslateX]); const style = useMemo(() => [_style, { left: SCREEN_WIDTH * indexInSwiper + (SCREEN_WIDTH / 2 - width / 2) }], [_style, indexInSwiper, width]); return ( // TODO: DEBUG IF renderToHardwareTextureAndroid={true} shouldRasterizeIOS={true} ARE A GOOD IDEAHeader.tsx
Package versions
I'm really not sure how I can make this more efficient or if I'm doing something wrong, so any tips would be greatly appreciated!
EDIT
Here's everything I've noted:
useAnimatedStyle
hook, that gets executed a lot (30 times for 30 different headers per on drag event), usingwithTiming
(orwithSpring
) is a bad idea. Instead, extracting that into auseSharedValue
and imperatively animating that when theisSwipeMode
orisCurrentHeader
props changes works a lot better. I am guessing it just takes a long time to set up thewithTiming
(orwithSpring
) animation, even though the inputs haven't changed an it already is at the desired position.useDerivedValue
s doesn't seem to really do anything, it feels a tiny bit smoother I guessremoveClippedSubviews
. (overflow hidden)useAnimatedStyle
hooks.