From 9fa90bb8d97db5078aedaa359d4b956d67e31ada Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 1 Dec 2023 02:11:05 +0000 Subject: [PATCH] Optimize pager rendering (#2055) * Pull out and memoize PagerTabBar * Avoid invalidating onScroll and add throttling * Make isScrolledDown update non-blocking * Fix types --- src/view/com/pager/PagerWithHeader.tsx | 179 ++++++++++++++++--------- 1 file changed, 117 insertions(+), 62 deletions(-) diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 2c7640c4..487c589e 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -11,14 +11,17 @@ import Animated, { useAnimatedStyle, useSharedValue, runOnJS, + runOnUI, scrollTo, useAnimatedRef, AnimatedRef, + SharedValue, } from 'react-native-reanimated' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {TabBar} from './TabBar' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' +import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' const SCROLLED_DOWN_LIMIT = 200 @@ -56,7 +59,6 @@ export const PagerWithHeader = React.forwardRef( }: PagerWithHeaderProps, ref, ) { - const {isMobile} = useWebMediaQueries() const [currentPage, setCurrentPage] = React.useState(0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) @@ -78,56 +80,34 @@ export const PagerWithHeader = React.forwardRef( [setHeaderOnlyHeight], ) - // render the the header and tab bar - const headerTransform = useAnimatedStyle(() => ({ - transform: [ - { - translateY: Math.min( - Math.min(scrollY.value, headerOnlyHeight) * -1, - 0, - ), - }, - ], - })) - const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( - - {renderHeader?.()} - - - - + ) }, [ + headerOnlyHeight, items, isHeaderReady, renderHeader, - headerTransform, currentPage, onCurrentPageSelected, - isMobile, onTabBarLayout, onHeaderOnlyLayout, + scrollY, testID, ], ) @@ -142,36 +122,50 @@ export const PagerWithHeader = React.forwardRef( } const lastForcedScrollY = useSharedValue(0) + const adjustScrollForOtherPages = () => { + 'worklet' + const currentScrollY = scrollY.value + const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) + if (lastForcedScrollY.value !== forcedScrollY) { + lastForcedScrollY.value = forcedScrollY + const refs = scrollRefs.value + for (let i = 0; i < refs.length; i++) { + if (i !== currentPage) { + // This needs to run on the UI thread. + scrollTo(refs[i], 0, forcedScrollY, false) + } + } + } + } + + const throttleTimeout = React.useRef | null>( + null, + ) + const queueThrottledOnScroll = useNonReactiveCallback(() => { + if (!throttleTimeout.current) { + throttleTimeout.current = setTimeout(() => { + throttleTimeout.current = null + + runOnUI(adjustScrollForOtherPages)() + + const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT + if (isScrolledDown !== nextIsScrolledDown) { + React.startTransition(() => { + setIsScrolledDown(nextIsScrolledDown) + }) + } + }, 80 /* Sync often enough you're unlikely to catch it unsynced */) + } + }) + const onScrollWorklet = React.useCallback( (e: NativeScrollEvent) => { 'worklet' const nextScrollY = e.contentOffset.y scrollY.value = nextScrollY - - const forcedScrollY = Math.min(nextScrollY, headerOnlyHeight) - if (lastForcedScrollY.value !== forcedScrollY) { - lastForcedScrollY.value = forcedScrollY - const refs = scrollRefs.value - for (let i = 0; i < refs.length; i++) { - if (i !== currentPage) { - scrollTo(refs[i], 0, forcedScrollY, false) - } - } - } - - const nextIsScrolledDown = nextScrollY > SCROLLED_DOWN_LIMIT - if (isScrolledDown !== nextIsScrolledDown) { - runOnJS(setIsScrolledDown)(nextIsScrolledDown) - } + runOnJS(queueThrottledOnScroll)() }, - [ - currentPage, - headerOnlyHeight, - isScrolledDown, - scrollRefs, - scrollY, - lastForcedScrollY, - ], + [scrollY, queueThrottledOnScroll], ) const onPageSelectedInner = React.useCallback( @@ -219,6 +213,67 @@ export const PagerWithHeader = React.forwardRef( }, ) +let PagerTabBar = ({ + currentPage, + headerOnlyHeight, + isHeaderReady, + items, + scrollY, + testID, + renderHeader, + onHeaderOnlyLayout, + onTabBarLayout, + onCurrentPageSelected, + onSelect, +}: { + currentPage: number + headerOnlyHeight: number + isHeaderReady: boolean + items: string[] + testID?: string + scrollY: SharedValue + renderHeader?: () => JSX.Element + onHeaderOnlyLayout: (e: LayoutChangeEvent) => void + onTabBarLayout: (e: LayoutChangeEvent) => void + onCurrentPageSelected?: (index: number) => void + onSelect?: (index: number) => void +}): React.ReactNode => { + const {isMobile} = useWebMediaQueries() + const headerTransform = useAnimatedStyle(() => ({ + transform: [ + { + translateY: Math.min(Math.min(scrollY.value, headerOnlyHeight) * -1, 0), + }, + ], + })) + return ( + + {renderHeader?.()} + + + + + ) +} +PagerTabBar = React.memo(PagerTabBar) + function PagerItem({ headerHeight, isReady,