[new arch] Fix profile perf (#8320)

* change approach for keeping scrollviews synced

* rm forwardRef from web too

---------

Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
Samuel Newman
2025-05-02 16:45:25 +03:00
parent 5fa61c9b27
commit 512951a480
3 changed files with 301 additions and 285 deletions
+30 -22
View File
@@ -1,16 +1,22 @@
import React, {forwardRef, useCallback, useContext} from 'react' import {
useCallback,
useContext,
useImperativeHandle,
useRef,
useState,
} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {DrawerGestureContext} from 'react-native-drawer-layout' import {DrawerGestureContext} from 'react-native-drawer-layout'
import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import PagerView, { import PagerView, {
PagerViewOnPageScrollEventData, type PagerViewOnPageScrollEventData,
PagerViewOnPageSelectedEvent, type PagerViewOnPageSelectedEvent,
PagerViewOnPageSelectedEventData, type PagerViewOnPageSelectedEventData,
PageScrollStateChangedNativeEventData, type PageScrollStateChangedNativeEventData,
} from 'react-native-pager-view' } from 'react-native-pager-view'
import Animated, { import Animated, {
runOnJS, runOnJS,
SharedValue, type SharedValue,
useEvent, useEvent,
useHandler, useHandler,
useSharedValue, useSharedValue,
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
interface Props { interface Props {
ref?: React.Ref<PagerRef>
initialPage?: number initialPage?: number
renderTabBar: RenderTabBarFn renderTabBar: RenderTabBarFn
// tab pressed, yet to scroll to page
onTabPressed?: (index: number) => void
// scroll settled
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
onPageScrollStateChanged?: ( onPageScrollStateChanged?: (
scrollState: 'idle' | 'dragging' | 'settling', scrollState: 'idle' | 'dragging' | 'settling',
@@ -47,22 +57,20 @@ interface Props {
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( export function Pager({
function PagerImpl( ref,
{
children, children,
initialPage = 0, initialPage = 0,
renderTabBar, renderTabBar,
onPageScrollStateChanged: parentOnPageScrollStateChanged,
onPageSelected: parentOnPageSelected, onPageSelected: parentOnPageSelected,
onTabPressed: parentOnTabPressed,
onPageScrollStateChanged: parentOnPageScrollStateChanged,
testID, testID,
}: React.PropsWithChildren<Props>, }: React.PropsWithChildren<Props>) {
ref, const [selectedPage, setSelectedPage] = useState(initialPage)
) { const pagerView = useRef<PagerView>(null)
const [selectedPage, setSelectedPage] = React.useState(initialPage)
const pagerView = React.useRef<PagerView>(null)
const [isIdle, setIsIdle] = React.useState(true) const [isIdle, setIsIdle] = useState(true)
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -74,13 +82,13 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
}, [setDrawerSwipeDisabled, selectedPage, isIdle]), }, [setDrawerSwipeDisabled, selectedPage, isIdle]),
) )
React.useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
setPage: (index: number) => { setPage: (index: number) => {
pagerView.current?.setPage(index) pagerView.current?.setPage(index)
}, },
})) }))
const onPageSelectedJSThread = React.useCallback( const onPageSelectedJSThread = useCallback(
(nextPosition: number) => { (nextPosition: number) => {
setSelectedPage(nextPosition) setSelectedPage(nextPosition)
parentOnPageSelected?.(nextPosition) parentOnPageSelected?.(nextPosition)
@@ -88,11 +96,12 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
[setSelectedPage, parentOnPageSelected], [setSelectedPage, parentOnPageSelected],
) )
const onTabBarSelect = React.useCallback( const onTabBarSelect = useCallback(
(index: number) => { (index: number) => {
parentOnTabPressed?.(index)
pagerView.current?.setPage(index) pagerView.current?.setPage(index)
}, },
[pagerView], [pagerView, parentOnTabPressed],
) )
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle') const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
@@ -153,8 +162,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
</GestureDetector> </GestureDetector>
</View> </View>
) )
}, }
)
function usePagerHandlers( function usePagerHandlers(
handlers: { handlers: {
+27 -14
View File
@@ -1,8 +1,19 @@
import React from 'react' import {
Children,
useCallback,
useImperativeHandle,
useRef,
useState,
} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {flushSync} from 'react-dom' import {flushSync} from 'react-dom'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {atoms as a} from '#/alf'
export interface PagerRef {
setPage: (index: number) => void
}
export interface RenderTabBarFnProps { export interface RenderTabBarFnProps {
selectedPage: number selectedPage: number
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
interface Props { interface Props {
ref?: React.Ref<PagerRef>
initialPage?: number initialPage?: number
renderTabBar: RenderTabBarFn renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
} }
export const Pager = React.forwardRef(function PagerImpl(
{ export function Pager({
ref,
children, children,
initialPage = 0, initialPage = 0,
renderTabBar, renderTabBar,
onPageSelected, onPageSelected,
}: React.PropsWithChildren<Props>, }: React.PropsWithChildren<Props>) {
ref, const [selectedPage, setSelectedPage] = useState(initialPage)
) { const scrollYs = useRef<Array<number | null>>([])
const [selectedPage, setSelectedPage] = React.useState(initialPage) const anchorRef = useRef(null)
const scrollYs = React.useRef<Array<number | null>>([])
const anchorRef = React.useRef(null)
React.useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
setPage: (index: number) => { setPage: (index: number) => {
onTabBarSelect(index) onTabBarSelect(index)
}, },
})) }))
const onTabBarSelect = React.useCallback( const onTabBarSelect = useCallback(
(index: number) => { (index: number) => {
const scrollY = window.scrollY const scrollY = window.scrollY
// We want to determine if the tabbar is already "sticking" at the top (in which // We want to determine if the tabbar is already "sticking" at the top (in which
@@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl(
tabBarAnchor: <View ref={anchorRef} />, tabBarAnchor: <View ref={anchorRef} />,
onSelect: e => onTabBarSelect(e), onSelect: e => onTabBarSelect(e),
})} })}
{React.Children.map(children, (child, i) => ( {Children.map(children, (child, i) => (
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}> <View
style={selectedPage === i ? a.flex_1 : a.hidden}
key={`page-${i}`}>
{child} {child}
</View> </View>
))} ))}
</View> </View>
) )
}) }
+42 -47
View File
@@ -1,17 +1,16 @@
import * as React from 'react' import {memo, useCallback, useEffect, useRef, useState} from 'react'
import { import {
LayoutChangeEvent, type LayoutChangeEvent,
NativeScrollEvent, type NativeScrollEvent,
ScrollView, type ScrollView,
StyleSheet, StyleSheet,
View, View,
} from 'react-native' } from 'react-native'
import Animated, { import Animated, {
AnimatedRef, type AnimatedRef,
runOnJS,
runOnUI, runOnUI,
scrollTo, scrollTo,
SharedValue, type SharedValue,
useAnimatedRef, useAnimatedRef,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
@@ -20,9 +19,13 @@ import Animated, {
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' import {
Pager,
type PagerRef,
type RenderTabBarFnProps,
} from '#/view/com/pager/Pager'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {ListMethods} from '../util/List' import {type ListMethods} from '../util/List'
import {PagerHeaderProvider} from './PagerHeaderContext' import {PagerHeaderProvider} from './PagerHeaderContext'
import {TabBar} from './TabBar' import {TabBar} from './TabBar'
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
} }
export interface PagerWithHeaderProps { export interface PagerWithHeaderProps {
ref?: React.Ref<PagerRef>
testID?: string testID?: string
children: children:
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
@@ -49,9 +53,8 @@ export interface PagerWithHeaderProps {
onCurrentPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void
allowHeaderOverScroll?: boolean allowHeaderOverScroll?: boolean
} }
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( export function PagerWithHeader({
function PageWithHeaderImpl( ref,
{
children, children,
testID, testID,
items, items,
@@ -61,12 +64,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
onPageSelected, onPageSelected,
onCurrentPageSelected, onCurrentPageSelected,
allowHeaderOverScroll, allowHeaderOverScroll,
}: PagerWithHeaderProps, }: PagerWithHeaderProps) {
ref, const [currentPage, setCurrentPage] = useState(0)
) { const [tabBarHeight, setTabBarHeight] = useState(0)
const [currentPage, setCurrentPage] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const scrollY = useSharedValue(0) const scrollY = useSharedValue(0)
const headerHeight = headerOnlyHeight + tabBarHeight const headerHeight = headerOnlyHeight + tabBarHeight
@@ -85,12 +86,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
} }
}) })
const renderTabBar = React.useCallback( const renderTabBar = useCallback(
(props: RenderTabBarFnProps) => { (props: RenderTabBarFnProps) => {
return ( return (
<PagerHeaderProvider <PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
scrollY={scrollY}
headerHeight={headerOnlyHeight}>
<PagerTabBar <PagerTabBar
headerOnlyHeight={headerOnlyHeight} headerOnlyHeight={headerOnlyHeight}
items={items} items={items}
@@ -126,7 +125,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
) )
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([]) const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
const registerRef = React.useCallback( const registerRef = useCallback(
(scrollRef: AnimatedRef<any> | null, atIndex: number) => { (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
scrollRefs.modify(refs => { scrollRefs.modify(refs => {
'worklet' 'worklet'
@@ -138,8 +137,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
) )
const lastForcedScrollY = useSharedValue(0) const lastForcedScrollY = useSharedValue(0)
const adjustScrollForOtherPages = () => { const adjustScrollForOtherPages = useCallback(
(scrollState: 'idle' | 'dragging' | 'settling') => {
'worklet' 'worklet'
if (scrollState !== 'dragging') return
const currentScrollY = scrollY.get() const currentScrollY = scrollY.get()
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
if (lastForcedScrollY.get() !== forcedScrollY) { if (lastForcedScrollY.get() !== forcedScrollY) {
@@ -152,21 +153,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
} }
} }
} }
} },
[currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY],
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
null,
) )
const queueThrottledOnScroll = useNonReactiveCallback(() => {
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)()
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
}
})
const onScrollWorklet = React.useCallback( const onScrollWorklet = useCallback(
(e: NativeScrollEvent) => { (e: NativeScrollEvent) => {
'worklet' 'worklet'
const nextScrollY = e.contentOffset.y const nextScrollY = e.contentOffset.y
@@ -177,13 +168,12 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
if (!isPossiblyInvalid) { if (!isPossiblyInvalid) {
scrollY.set(nextScrollY) scrollY.set(nextScrollY)
runOnJS(queueThrottledOnScroll)()
} }
}, },
[scrollY, queueThrottledOnScroll, headerHeight], [scrollY, headerHeight],
) )
const onPageSelectedInner = React.useCallback( const onPageSelectedInner = useCallback(
(index: number) => { (index: number) => {
setCurrentPage(index) setCurrentPage(index)
onPageSelected?.(index) onPageSelected?.(index)
@@ -191,13 +181,19 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
[onPageSelected, setCurrentPage], [onPageSelected, setCurrentPage],
) )
const onTabPressed = useCallback(() => {
runOnUI(adjustScrollForOtherPages)('dragging')
}, [adjustScrollForOtherPages])
return ( return (
<Pager <Pager
ref={ref} ref={ref}
testID={testID} testID={testID}
initialPage={initialPage} initialPage={initialPage}
onTabPressed={onTabPressed}
onPageSelected={onPageSelectedInner} onPageSelected={onPageSelectedInner}
renderTabBar={renderTabBar}> renderTabBar={renderTabBar}
onPageScrollStateChanged={adjustScrollForOtherPages}>
{toArray(children) {toArray(children)
.filter(Boolean) .filter(Boolean)
.map((child, i) => { .map((child, i) => {
@@ -219,8 +215,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
})} })}
</Pager> </Pager>
) )
}, }
)
let PagerTabBar = ({ let PagerTabBar = ({
currentPage, currentPage,
@@ -258,7 +253,7 @@ let PagerTabBar = ({
dragState: SharedValue<'idle' | 'dragging' | 'settling'> dragState: SharedValue<'idle' | 'dragging' | 'settling'>
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0) const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
const headerTransform = useAnimatedStyle(() => { const headerTransform = useAnimatedStyle(() => {
const translateY = const translateY =
Math.min( Math.min(
@@ -275,7 +270,7 @@ let PagerTabBar = ({
], ],
} }
}) })
const headerRef = React.useRef(null) const headerRef = useRef(null)
return ( return (
<Animated.View <Animated.View
pointerEvents={isIOS ? 'auto' : 'box-none'} pointerEvents={isIOS ? 'auto' : 'box-none'}
@@ -327,7 +322,7 @@ let PagerTabBar = ({
</Animated.View> </Animated.View>
) )
} }
PagerTabBar = React.memo(PagerTabBar) PagerTabBar = memo(PagerTabBar)
function PagerItem({ function PagerItem({
headerHeight, headerHeight,
@@ -348,7 +343,7 @@ function PagerItem({
}) { }) {
const scrollElRef = useAnimatedRef() const scrollElRef = useAnimatedRef()
React.useEffect(() => { useEffect(() => {
registerRef(scrollElRef, index) registerRef(scrollElRef, index)
return () => { return () => {
registerRef(null, index) registerRef(null, index)