fix fabric reanimated perf by introducing derived value

This commit is contained in:
Samuel Newman
2025-12-31 15:59:13 +02:00
parent 8edc42293f
commit f117c1b801
5 changed files with 48 additions and 42 deletions
+2 -2
View File
@@ -376,8 +376,8 @@
"reanimated": {
"staticFeatureFlags": {
"DISABLE_COMMIT_PAUSING_MECHANISM": true,
"ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS": true,
"IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS": true,
"ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS": false,
"IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS": false,
"USE_SYNCHRONIZABLE_FOR_MUTABLES": true,
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true,
"FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": true
@@ -1,6 +1,5 @@
import {type StyleProp, View, type ViewStyle} from 'react-native'
import Animated, {
Extrapolation,
interpolate,
type SharedValue,
useAnimatedStyle,
@@ -24,30 +23,28 @@ export function GrowableAvatar({
return <View style={style}>{children}</View>
}
const {scrollY} = pagerContext
const {clampedScrollY} = pagerContext
return (
<GrowableAvatarInner scrollY={scrollY} style={style}>
<GrowableAvatarInner clampedScrollY={clampedScrollY} style={style}>
{children}
</GrowableAvatarInner>
)
}
function GrowableAvatarInner({
scrollY,
clampedScrollY,
children,
style,
}: {
scrollY: SharedValue<number>
clampedScrollY: SharedValue<number>
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], {
extrapolateRight: Extrapolation.CLAMP,
}),
scale: interpolate(clampedScrollY.get(), [-150, 0], [1.2, 1]),
},
],
}))
+19 -22
View File
@@ -44,34 +44,37 @@ export function GrowableBanner({
)
}
const {scrollY} = pagerContext
const {clampedScrollY} = pagerContext
return (
<GrowableBannerInner scrollY={scrollY} backButton={backButton}>
<GrowableBannerInner
clampedScrollY={clampedScrollY}
backButton={backButton}>
{children}
</GrowableBannerInner>
)
}
function GrowableBannerInner({
scrollY,
clampedScrollY,
backButton,
children,
}: {
scrollY: SharedValue<number>
clampedScrollY: SharedValue<number>
backButton?: React.ReactNode
children: React.ReactNode
}) {
const {top: topInset} = useSafeAreaInsets()
const isFetching = useIsProfileFetching()
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
const animateSpinner = useShouldAnimateSpinner({
isFetching,
clampedScrollY,
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
scale: interpolate(scrollY.get(), [-150, 0], [2, 1], {
extrapolateRight: Extrapolation.CLAMP,
}),
scale: interpolate(clampedScrollY.get(), [-150, 0], [2, 1]),
},
],
}))
@@ -79,7 +82,7 @@ function GrowableBannerInner({
const animatedBlurViewProps = useAnimatedProps(() => {
return {
intensity: interpolate(
scrollY.get(),
clampedScrollY.get(),
[-300, -65, -15],
[50, 40, 0],
Extrapolation.CLAMP,
@@ -88,7 +91,7 @@ function GrowableBannerInner({
})
const animatedSpinnerStyle = useAnimatedStyle(() => {
const scrollYValue = scrollY.get()
const scrollYValue = clampedScrollY.get()
return {
display: scrollYValue < 0 ? 'flex' : 'none',
opacity: interpolate(
@@ -98,11 +101,7 @@ function GrowableBannerInner({
Extrapolation.CLAMP,
),
transform: [
{
translateY: interpolate(scrollYValue, [-150, 0], [-75, 0], {
extrapolateRight: Extrapolation.CLAMP,
}),
},
{translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])},
{rotate: '90deg'},
],
}
@@ -111,9 +110,7 @@ function GrowableBannerInner({
const animatedBackButtonStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], {
extrapolateRight: Extrapolation.CLAMP,
}),
translateY: Math.max(-150, Math.min(10, clampedScrollY.get())),
},
],
}))
@@ -172,10 +169,10 @@ function useIsProfileFetching() {
function useShouldAnimateSpinner({
isFetching,
scrollY,
clampedScrollY,
}: {
isFetching: boolean
scrollY: SharedValue<number>
clampedScrollY: SharedValue<number>
}) {
const [isOverscrolled, setIsOverscrolled] = useState(false)
// HACK: it reports a scroll pos of 0 for a tick when fetching finishes
@@ -183,13 +180,13 @@ function useShouldAnimateSpinner({
const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
useAnimatedReaction(
() => scrollY.get() < -5,
() => clampedScrollY.get() < -5,
(value, prevValue) => {
if (value !== prevValue) {
runOnJS(setIsOverscrolled)(value)
}
},
[scrollY],
[clampedScrollY],
)
const [isAnimating, setIsAnimating] = useState(isFetching)
@@ -14,8 +14,8 @@ export function StatusBarShadow() {
const pagerContext = usePagerHeaderContext()
if (isIOS && pagerContext) {
const {scrollY} = pagerContext
return <StatusBarShadowInnner scrollY={scrollY} />
const {clampedScrollY} = pagerContext
return <StatusBarShadowInnner clampedScrollY={clampedScrollY} />
}
return (
@@ -30,14 +30,18 @@ export function StatusBarShadow() {
)
}
function StatusBarShadowInnner({scrollY}: {scrollY: SharedValue<number>}) {
function StatusBarShadowInnner({
clampedScrollY,
}: {
clampedScrollY: SharedValue<number>
}) {
const {top: topInset} = useSafeAreaInsets()
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: Math.min(0, scrollY.get()),
translateY: Math.min(0, clampedScrollY.get()),
},
],
}
+14 -6
View File
@@ -1,10 +1,11 @@
import React, {useContext} from 'react'
import {type SharedValue} from 'react-native-reanimated'
import {createContext, useContext, useMemo} from 'react'
import {type SharedValue, useDerivedValue} from 'react-native-reanimated'
import {isNative} from '#/platform/detection'
export const PagerHeaderContext = React.createContext<{
export const PagerHeaderContext = createContext<{
scrollY: SharedValue<number>
clampedScrollY: SharedValue<number>
headerHeight: number
} | null>(null)
PagerHeaderContext.displayName = 'PagerHeaderContext'
@@ -24,9 +25,16 @@ export function PagerHeaderProvider({
headerHeight: number
children: React.ReactNode
}) {
const value = React.useMemo(
() => ({scrollY, headerHeight}),
[scrollY, headerHeight],
// bit of a hackfix - most of the usage of scrollY here is for the pull-to-refresh
// behaviour in the header, and after scrolling it's not needed anymore.
// to improve performance, we create a derived value that clamps up-front,
// therefore removing the need for the downstream animated styles to run on every frame
// when they won't actually be used -sfn
const clampedScrollY = useDerivedValue(() => Math.min(100, scrollY.get()))
const value = useMemo(
() => ({scrollY, clampedScrollY, headerHeight}),
[scrollY, headerHeight, clampedScrollY],
)
return (
<PagerHeaderContext.Provider value={value}>