From daed047bb41bcdac374398b06f87895511ea34a8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Sep 2025 18:39:28 +0300 Subject: [Perf] Drawer gesture perf fix + related cleanup (#8953) * split drawer layout into own component * don't put props in dep array * memoize pager view --- src/state/shell/drawer-open.tsx | 12 ++-- src/view/com/home/HomeHeader.tsx | 14 ++-- src/view/com/pager/Pager.tsx | 31 ++++++--- src/view/shell/index.tsx | 135 +++++++++++++++++++++------------------ 4 files changed, 107 insertions(+), 85 deletions(-) diff --git a/src/state/shell/drawer-open.tsx b/src/state/shell/drawer-open.tsx index 87650a09c..7ce4189c3 100644 --- a/src/state/shell/drawer-open.tsx +++ b/src/state/shell/drawer-open.tsx @@ -1,15 +1,15 @@ -import React from 'react' +import {createContext, useContext, useState} from 'react' type StateContext = boolean type SetContext = (v: boolean) => void -const stateContext = React.createContext(false) +const stateContext = createContext(false) stateContext.displayName = 'DrawerOpenStateContext' -const setContext = React.createContext((_: boolean) => {}) +const setContext = createContext((_: boolean) => {}) setContext.displayName = 'DrawerOpenSetContext' export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = React.useState(false) + const [state, setState] = useState(false) return ( @@ -19,9 +19,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } export function useIsDrawerOpen() { - return React.useContext(stateContext) + return useContext(stateContext) } export function useSetDrawerOpen() { - return React.useContext(setContext) + return useContext(setContext) } diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx index 0ec9ac753..4ae344549 100644 --- a/src/view/com/home/HomeHeader.tsx +++ b/src/view/com/home/HomeHeader.tsx @@ -1,10 +1,10 @@ import React from 'react' import {useNavigation} from '@react-navigation/native' -import {NavigationProp} from '#/lib/routes/types' -import {FeedSourceInfo} from '#/state/queries/feed' +import {type NavigationProp} from '#/lib/routes/types' +import {type FeedSourceInfo} from '#/state/queries/feed' import {useSession} from '#/state/session' -import {RenderTabBarFnProps} from '#/view/com/pager/Pager' +import {type RenderTabBarFnProps} from '#/view/com/pager/Pager' import {TabBar} from '../pager/TabBar' import {HomeHeaderLayout} from './HomeHeaderLayout' @@ -15,7 +15,7 @@ export function HomeHeader( feeds: FeedSourceInfo[] }, ) { - const {feeds} = props + const {feeds, onSelect: onSelectProp} = props const {hasSession} = useSession() const navigation = useNavigation() @@ -43,11 +43,11 @@ export function HomeHeader( (index: number) => { if (!hasPinnedCustom && index === items.length - 1) { onPressFeedsLink() - } else if (props.onSelect) { - props.onSelect(index) + } else if (onSelectProp) { + onSelectProp(index) } }, - [items.length, onPressFeedsLink, props, hasPinnedCustom], + [items.length, onPressFeedsLink, onSelectProp, hasPinnedCustom], ) return ( diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 8cc346903..5eb7d7608 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -1,7 +1,9 @@ import { + memo, useCallback, useContext, useImperativeHandle, + useMemo, useRef, useState, } from 'react' @@ -56,6 +58,7 @@ interface Props { } const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) +const MemoizedAnimatedPagerView = memo(AnimatedPagerView) export function Pager({ ref, @@ -139,10 +142,6 @@ export function Pager({ [parentOnPageScrollStateChanged], ) - const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web - const nativeGesture = - Gesture.Native().requireExternalGestureToFail(drawerGesture) - return ( {renderTabBar({ @@ -151,19 +150,33 @@ export function Pager({ dragProgress, dragState, })} - - + {children} - - + + ) } +function DrawerGestureRequireFail({children}: {children: React.ReactNode}) { + const drawerGesture = useContext(DrawerGestureContext) + + const nativeGesture = useMemo(() => { + const gesture = Gesture.Native() + if (drawerGesture) { + gesture.requireExternalGestureToFail(drawerGesture) + } + return gesture + }, [drawerGesture]) + + return {children} +} + function usePagerHandlers( handlers: { onPageScroll: (e: PagerViewOnPageScrollEventData) => void diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 277e5c523..5075f05cb 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -45,25 +45,10 @@ import {Composer} from './Composer' import {DrawerContent} from './Drawer' function ShellInner() { - const t = useTheme() - const isDrawerOpen = useIsDrawerOpen() - const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() - const setIsDrawerOpen = useSetDrawerOpen() const winDim = useWindowDimensions() const insets = useSafeAreaInsets() const {state: policyUpdateState} = usePolicyUpdateContext() - const renderDrawerContent = useCallback(() => , []) - const onOpenDrawer = useCallback( - () => setIsDrawerOpen(true), - [setIsDrawerOpen], - ) - const onCloseDrawer = useCallback( - () => setIsDrawerOpen(false), - [setIsDrawerOpen], - ) - const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) - const {hasSession} = useSession() const closeAnyActiveElement = useCloseAnyActiveElement() useNotificationsRegistration() @@ -102,60 +87,14 @@ function ShellInner() { } }, [dedupe, navigation]) - const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled - const [trendingScrollGesture] = useState(() => Gesture.Native()) return ( <> - { - handler = handler.requireExternalGestureToFail( - trendingScrollGesture, - ) - - if (swipeEnabled) { - if (isDrawerOpen) { - return handler.activeOffsetX([-1, 1]) - } else { - return ( - handler - // Any movement to the left is a pager swipe - // so fail the drawer gesture immediately. - .failOffsetX(-1) - // Don't rush declaring that a movement to the right - // is a drawer swipe. It could be a vertical scroll. - .activeOffsetX(5) - ) - } - } else { - // Fail the gesture immediately. - // This seems more reliable than the `swipeEnabled` prop. - // With `swipeEnabled` alone, the gesture may freeze after toggling off/on. - return handler.failOffsetX([0, 0]).failOffsetY([0, 0]) - } - }} - open={isDrawerOpen} - onOpen={onOpenDrawer} - onClose={onCloseDrawer} - swipeEdgeWidth={winDim.width} - swipeMinVelocity={100} - swipeMinDistance={10} - drawerType={isIOS ? 'slide' : 'front'} - overlayStyle={{ - backgroundColor: select(t.name, { - light: 'rgba(0, 57, 117, 0.1)', - dark: isAndroid - ? 'rgba(16, 133, 254, 0.1)' - : 'rgba(1, 82, 168, 0.1)', - dim: 'rgba(10, 13, 16, 0.8)', - }), - }}> + - + @@ -182,6 +121,76 @@ function ShellInner() { ) } +function DrawerLayout({children}: {children: React.ReactNode}) { + const t = useTheme() + const isDrawerOpen = useIsDrawerOpen() + const setIsDrawerOpen = useSetDrawerOpen() + const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() + const winDim = useWindowDimensions() + + const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) + const {hasSession} = useSession() + + const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled + const [trendingScrollGesture] = useState(() => Gesture.Native()) + + const renderDrawerContent = useCallback(() => , []) + const onOpenDrawer = useCallback( + () => setIsDrawerOpen(true), + [setIsDrawerOpen], + ) + const onCloseDrawer = useCallback( + () => setIsDrawerOpen(false), + [setIsDrawerOpen], + ) + + return ( + { + handler = handler.requireExternalGestureToFail(trendingScrollGesture) + + if (swipeEnabled) { + if (isDrawerOpen) { + return handler.activeOffsetX([-1, 1]) + } else { + return ( + handler + // Any movement to the left is a pager swipe + // so fail the drawer gesture immediately. + .failOffsetX(-1) + // Don't rush declaring that a movement to the right + // is a drawer swipe. It could be a vertical scroll. + .activeOffsetX(5) + ) + } + } else { + // Fail the gesture immediately. + // This seems more reliable than the `swipeEnabled` prop. + // With `swipeEnabled` alone, the gesture may freeze after toggling off/on. + return handler.failOffsetX([0, 0]).failOffsetY([0, 0]) + } + }} + open={isDrawerOpen} + onOpen={onOpenDrawer} + onClose={onCloseDrawer} + swipeEdgeWidth={winDim.width} + swipeMinVelocity={100} + swipeMinDistance={10} + drawerType={isIOS ? 'slide' : 'front'} + overlayStyle={{ + backgroundColor: select(t.name, { + light: 'rgba(0, 57, 117, 0.1)', + dark: isAndroid ? 'rgba(16, 133, 254, 0.1)' : 'rgba(1, 82, 168, 0.1)', + dim: 'rgba(10, 13, 16, 0.8)', + }), + }}> + {children} + + ) +} + export function Shell() { const t = useTheme() const {status: geolocation} = useGeolocationStatus() -- cgit 1.4.1