Skip to content

Commit

Permalink
perf: optimize and reduce rerenders (#186)
Browse files Browse the repository at this point in the history
* perf: reduce tabbar rerenders with scrollEnabled

* perf: remove 1 scrollable rerender

* perf: use more animated values

* perf: memoize scrollables
  • Loading branch information
andreialecu committed Jul 28, 2021
1 parent ebfecd7 commit a9d2a44
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 133 deletions.
7 changes: 5 additions & 2 deletions example/src/Shared/QuickStartDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { Tabs } from 'react-native-collapsible-tab-view'

const HEADER_HEIGHT = 250

const DATA = [0, 1, 2, 3, 4]
const identity = (v: unknown): string => v + ''

const Header = () => {
return <View style={styles.header} />
}
Expand All @@ -22,9 +25,9 @@ const Example: React.FC = () => {
>
<Tabs.Tab name="A">
<Tabs.FlatList
data={[0, 1, 2, 3, 4]}
data={DATA}
renderItem={renderItem}
keyExtractor={(v) => v + ''}
keyExtractor={identity}
/>
</Tabs.Tab>
<Tabs.Tab name="B">
Expand Down
47 changes: 25 additions & 22 deletions src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,22 @@ export const Container = React.memo(
const windowWidth = useWindowDimensions().width
const firstRender = React.useRef(true)

const [containerHeight, setContainerHeight] = React.useState<
number | undefined
>(undefined)
const [tabBarHeight, setTabBarHeight] = React.useState<
number | undefined
>(initialTabBarHeight)
const [headerHeight, setHeaderHeight] = React.useState<
number | undefined
>(!renderHeader ? 0 : initialHeaderHeight)

const contentInset = React.useMemo(
() => (IS_IOS ? (headerHeight || 0) + (tabBarHeight || 0) : 0),
[headerHeight, tabBarHeight]
const containerHeight = useSharedValue<number | undefined>(undefined)

const tabBarHeight = useSharedValue<number | undefined>(
initialTabBarHeight
)

const headerHeight = useSharedValue<number | undefined>(
!renderHeader ? 0 : initialHeaderHeight
)

const contentInset = useDerivedValue(() => {
return IS_IOS
? (headerHeight.value || 0) + (tabBarHeight.value || 0)
: 0
})

const isSwiping = useSharedValue(false)
const isSnapping: ContextType['isSnapping'] = useSharedValue(false)
const snappingTo: ContextType['snappingTo'] = useSharedValue(0)
Expand Down Expand Up @@ -156,7 +157,9 @@ export const Container = React.memo(
}, [tabNames])
const calculateNextOffset = useSharedValue(index.value)
const headerScrollDistance: ContextType['headerScrollDistance'] = useDerivedValue(() => {
return headerHeight !== undefined ? headerHeight - minHeaderHeight : 0
return headerHeight.value !== undefined
? headerHeight.value - minHeaderHeight
: 0
}, [headerHeight, minHeaderHeight])

const getItemLayout = React.useCallback(
Expand Down Expand Up @@ -218,7 +221,7 @@ export const Container = React.memo(
scrollToImpl(
refMap[name],
0,
scrollY.value[index.value] - contentInset,
scrollY.value[index.value] - contentInset.value,
false
)
})
Expand Down Expand Up @@ -334,8 +337,8 @@ export const Container = React.memo(
const getHeaderHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (headerHeight !== height) {
setHeaderHeight(height)
if (headerHeight.value !== height) {
headerHeight.value = height
}
},
[headerHeight]
Expand All @@ -344,15 +347,15 @@ export const Container = React.memo(
const getTabBarHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (tabBarHeight !== height) setTabBarHeight(height)
if (tabBarHeight.value !== height) tabBarHeight.value = height
},
[tabBarHeight]
)

const onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (containerHeight !== height) setContainerHeight(height)
if (containerHeight.value !== height) containerHeight.value = height
},
[containerHeight]
)
Expand Down Expand Up @@ -393,7 +396,7 @@ export const Container = React.memo(
runOnUI(scrollToImpl)(
ref,
0,
headerScrollDistance.value - contentInset,
headerScrollDistance.value - contentInset.value,
true
)
} else {
Expand Down Expand Up @@ -442,8 +445,8 @@ export const Container = React.memo(
<Context.Provider
value={{
contentInset,
tabBarHeight: tabBarHeight || 0,
headerHeight: headerHeight || 0,
tabBarHeight,
headerHeight,
refMap,
tabNames,
index,
Expand Down
81 changes: 60 additions & 21 deletions src/FlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ import {
useUpdateScrollViewContentSize,
} from './hooks'

/**
* Used as a memo to prevent rerendering too often when the context changes.
* See: https://github.com/facebook/react/issues/15156#issuecomment-474590693
*/
const FlatListMemo = React.memo(
React.forwardRef<RNFlatList, React.PropsWithChildren<FlatListProps<unknown>>>(
(props, passRef) => {
return (
<AnimatedFlatList
// @ts-expect-error reanimated types are broken on ref
ref={passRef}
{...props}
/>
)
}
)
)

function FlatListImpl<R>(
{
contentContainerStyle,
Expand All @@ -26,15 +44,14 @@ function FlatListImpl<R>(
const name = useTabNameContext()
const { setRef, contentInset, scrollYCurrent } = useTabsContext()
const ref = useSharedAnimatedRef<RNFlatList<unknown>>(passRef)
const [canBindScrollEvent, setCanBindScrollEvent] = React.useState(false)

const { scrollHandler, enable } = useScrollHandlerY(name)
useAfterMountEffect(() => {
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
setCanBindScrollEvent(true)
enable(true)
})

const scrollHandler = useScrollHandlerY(name, { enabled: canBindScrollEvent })
const {
style: _style,
contentContainerStyle: _contentContainerStyle,
Expand All @@ -50,35 +67,57 @@ function FlatListImpl<R>(
})

const scrollContentSizeChangeHandlers = useChainCallback(
scrollContentSizeChange,
onContentSizeChange
React.useMemo(() => [scrollContentSizeChange, onContentSizeChange], [
onContentSizeChange,
scrollContentSizeChange,
])
)

const memoRefreshControl = React.useMemo(
() =>
refreshControl &&
React.cloneElement(refreshControl, {
progressViewOffset,
...refreshControl.props,
}),
[progressViewOffset, refreshControl]
)
const memoContentOffset = React.useMemo(
() => ({
y: IS_IOS ? -contentInset.value + scrollYCurrent.value : 0,
x: 0,
}),
[contentInset.value, scrollYCurrent.value]
)
const memoContentInset = React.useMemo(() => ({ top: contentInset.value }), [
contentInset.value,
])
const memoContentContainerStyle = React.useMemo(
() => [
_contentContainerStyle,
// TODO: investigate types
contentContainerStyle as any,
],
[_contentContainerStyle, contentContainerStyle]
)
const memoStyle = React.useMemo(() => [_style, style], [_style, style])

return (
<AnimatedFlatList
// @ts-expect-error typescript complains about `unknown` in the memo, it should be T
<FlatListMemo
{...rest}
// @ts-expect-error problem with reanimated types, they're missing `ref`
ref={ref}
bouncesZoom={false}
style={[_style, style]}
contentContainerStyle={[_contentContainerStyle, contentContainerStyle]}
style={memoStyle}
contentContainerStyle={memoContentContainerStyle}
progressViewOffset={progressViewOffset}
onScroll={scrollHandler}
onContentSizeChange={scrollContentSizeChangeHandlers}
scrollEventThrottle={16}
contentInset={{ top: contentInset }}
contentOffset={{
y: IS_IOS ? -contentInset + scrollYCurrent.value : 0,
x: 0,
}}
contentInset={memoContentInset}
contentOffset={memoContentOffset}
automaticallyAdjustContentInsets={false}
refreshControl={
refreshControl &&
React.cloneElement(refreshControl, {
progressViewOffset,
...refreshControl.props,
})
}
refreshControl={memoRefreshControl}
/>
)
}
Expand Down
39 changes: 17 additions & 22 deletions src/MaterialTabBar/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ const MaterialTabBar = <T extends TabName = any>({
const tabBarRef = useAnimatedRef<Animated.ScrollView>()
const windowWidth = useWindowDimensions().width
const isFirstRender = React.useRef(true)
const [itemsLayoutGathering, setItemsLayoutGathering] = React.useState(
new Map<T, ItemLayout>()
)
const itemLayoutGathering = React.useRef(new Map<T, ItemLayout>())

const tabsOffset = useSharedValue(0)
const isScrolling = useSharedValue(false)

Expand Down Expand Up @@ -97,30 +96,26 @@ const MaterialTabBar = <T extends TabName = any>({
if (scrollEnabled) {
if (!event.nativeEvent?.layout) return
const { width, x } = event.nativeEvent.layout
setItemsLayoutGathering((itemsLayoutGathering) => {
const update = new Map(itemsLayoutGathering)
return update.set(name, {
width,
x,
})

itemLayoutGathering.current.set(name, {
width,
x,
})

// pick out the layouts for the tabs we know about (in case they changed dynamically)
const layout = Array.from(itemLayoutGathering.current.entries())
.filter(([tabName]) => tabNames.includes(tabName))
.map(([, layout]) => layout)
.sort((a, b) => a.x - b.x)

if (layout.length === tabNames.length) {
setItemsLayout(layout)
}
}
},
[scrollEnabled]
[scrollEnabled, tabNames]
)

React.useEffect(() => {
// pick out the layouts for the tabs we know about (in case they changed dynamically)
const layout = Array.from(itemsLayoutGathering.entries())
.filter(([tabName]) => tabNames.includes(tabName))
.map(([, layout]) => layout)
.sort((a, b) => a.x - b.x)

if (layout.length === tabNames.length) {
setItemsLayout(layout)
}
}, [itemsLayoutGathering, tabNames])

const cancelNextScrollSync = useSharedValue(index.value)

const onScroll = useAnimatedScrollHandler(
Expand Down

0 comments on commit a9d2a44

Please sign in to comment.