Skip to content

Commit a758e43

Browse files
committed
perf: optimize performance in dev mode
Signed-off-by: Innei <tukon479@gmail.com>
1 parent 49f3cc2 commit a758e43

File tree

16 files changed

+186
-180
lines changed

16 files changed

+186
-180
lines changed

apps/mobile/src/lib/navigation/WrappedScreenItem.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PrimitiveAtom } from "jotai"
33
import { atom, useAtomValue, useSetAtom } from "jotai"
44
import type { FC, ReactNode } from "react"
55
import { memo, useCallback, useContext, useMemo, useRef } from "react"
6-
import type { NativeSyntheticEvent } from "react-native"
6+
import type { NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"
77
import { StyleSheet, View } from "react-native"
88
import { useSharedValue } from "react-native-reanimated"
99
import type { ScreenStackHeaderConfigProps, StackPresentationTypes } from "react-native-screens"
@@ -33,6 +33,7 @@ export const WrappedScreenItem: FC<
3333

3434
headerConfig?: ScreenStackHeaderConfigProps
3535
screenOptions?: NavigationControllerViewExtraProps
36+
style?: StyleProp<ViewStyle>
3637
} & ScreenOptionsContextType
3738
> = memo(
3839
({
@@ -41,6 +42,7 @@ export const WrappedScreenItem: FC<
4142
stackPresentation,
4243
headerConfig,
4344
screenOptions: screenOptionsProp,
45+
style,
4446
...rest
4547
}) => {
4648
const navigation = useNavigation()
@@ -143,6 +145,7 @@ export const WrappedScreenItem: FC<
143145
style={[
144146
StyleSheet.absoluteFill,
145147
{ backgroundColor: screenOptionsProp?.transparent ? undefined : backgroundColor },
148+
style,
146149
]}
147150
{...rest}
148151
{...mergedScreenOptions}

apps/mobile/src/lib/navigation/bottom-tab/TabScreen.ios.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,15 @@ export const TabScreen: FC<PropsWithChildren<Omit<TabScreenProps, "tabScreenInde
9595
)
9696
const shouldLoadReact = mergedProps.lazy ? isSelected || isLoadedBefore : true
9797

98+
const optimize = __DEV__ && isSelected
9899
return (
99100
<TabScreenNative style={StyleSheet.absoluteFill}>
100101
<TabScreenContext.Provider value={ctxValue}>
101-
{shouldLoadReact && (
102+
{shouldLoadReact && optimize && (
102103
<WrappedScreenItem screenId={`tab-screen-${tabScreenIndex}`}>
103104
{children}
104105
<ScreenNameRegister />
105106
<LifecycleEvents isSelected={isSelected} />
106-
{/* <CalculateTabBarOpacity /> */}
107107
</WrappedScreenItem>
108108
)}
109109
</TabScreenContext.Provider>

apps/mobile/src/modules/context-menu/feeds.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ const PreviewFeeds = (props: { id: string; view: FeedViewType }) => {
328328
({ item: id }: ListRenderItemInfo<string>) => (
329329
<EntryNormalItem entryId={id} extraData="" view={props.view} />
330330
),
331-
[],
331+
[props.view],
332332
)
333333
return (
334334
<View className="bg-system-background size-full flex-1">

apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FeedViewType } from "@follow/constants"
22
import type { ListRenderItemInfo } from "@shopify/flash-list"
33
import type { ElementRef } from "react"
4-
import { forwardRef, useCallback, useMemo } from "react"
4+
import { forwardRef, useCallback, useImperativeHandle, useMemo } from "react"
55
import { View } from "react-native"
66

77
import { usePlayingUrl } from "@/src/lib/player"
@@ -10,14 +10,14 @@ import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
1010
import { useFetchEntriesControls } from "../screen/atoms"
1111
import { TimelineSelectorList } from "../screen/TimelineSelectorList"
1212
import { EntryListFooter } from "./EntryListFooter"
13-
import { useOnViewableItemsChanged } from "./hooks"
13+
import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
1414
import { ItemSeparator } from "./ItemSeparator"
1515
import { EntryNormalItem } from "./templates/EntryNormalItem"
1616

1717
export const EntryListContentArticle = forwardRef<
1818
ElementRef<typeof TimelineSelectorList>,
1919
{ entryIds: string[] | null; active?: boolean; view: FeedViewType }
20-
>(({ entryIds, active, view }, ref) => {
20+
>(({ entryIds, active, view }, forwardRef) => {
2121
const playingAudioUrl = usePlayingUrl()
2222

2323
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage } =
@@ -35,10 +35,15 @@ export const EntryListContentArticle = forwardRef<
3535
[hasNextPage],
3636
)
3737

38+
const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
39+
3840
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
3941
disabled: active === false || isFetching,
42+
onScroll: hackOnScroll,
4043
})
4144

45+
useImperativeHandle(forwardRef, () => ref.current!)
46+
4247
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
4348

4449
return (
@@ -48,18 +53,21 @@ export const EntryListContentArticle = forwardRef<
4853
isRefetching={isRefetching}
4954
data={entryIds}
5055
extraData={playingAudioUrl}
51-
keyExtractor={(id) => id}
56+
keyExtractor={defaultKeyExtractor}
5257
estimatedItemSize={100}
5358
renderItem={renderItem}
5459
onEndReached={fetchNextPage}
5560
onScroll={onScroll}
5661
onViewableItemsChanged={onViewableItemsChanged}
5762
ItemSeparatorComponent={ItemSeparator}
5863
ListFooterComponent={ListFooterComponent}
64+
style={hackStyle}
5965
/>
6066
)
6167
})
6268

69+
const defaultKeyExtractor = (id: string) => id
70+
6371
export function EntryItemSkeleton() {
6472
return (
6573
<View className="bg-system-background flex flex-row items-center p-4">

apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useTypeScriptHappyCallback } from "@follow/hooks"
22
import type { MasonryFlashListProps } from "@shopify/flash-list"
33
import type { ElementRef } from "react"
4-
import { forwardRef } from "react"
4+
import { forwardRef, useImperativeHandle } from "react"
55
import { View } from "react-native"
66

77
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
@@ -10,7 +10,7 @@ import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
1010

1111
import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList"
1212
import { GridEntryListFooter } from "./EntryListFooter"
13-
import { useOnViewableItemsChanged } from "./hooks"
13+
import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
1414
// import type { MasonryItem } from "./templates/EntryGridItem"
1515
import { EntryPictureItem } from "./templates/EntryPictureItem"
1616

@@ -20,11 +20,14 @@ export const EntryListContentPicture = forwardRef<
2020
MasonryFlashListProps<string>,
2121
"data" | "renderItem"
2222
>
23-
>(({ entryIds, active, ...rest }, ref) => {
23+
>(({ entryIds, active, ...rest }, forwardRef) => {
24+
const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
25+
useImperativeHandle(forwardRef, () => ref.current!)
2426
const { fetchNextPage, refetch, isRefetching, hasNextPage, isFetching } =
2527
useFetchEntriesControls()
2628
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
2729
disabled: active === false || isFetching,
30+
onScroll: hackOnScroll,
2831
})
2932

3033
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
@@ -42,6 +45,7 @@ export const EntryListContentPicture = forwardRef<
4245
onScroll={onScroll}
4346
onEndReached={fetchNextPage}
4447
numColumns={2}
48+
style={hackStyle}
4549
estimatedItemSize={100}
4650
ListFooterComponent={
4751
hasNextPage ? (

apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import type { ListRenderItemInfo } from "@shopify/flash-list"
22
import type { ElementRef } from "react"
3-
import { forwardRef, useCallback, useMemo } from "react"
3+
import { forwardRef, useCallback, useImperativeHandle, useMemo } from "react"
44
import { View } from "react-native"
55

66
import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
77

88
import { useFetchEntriesControls } from "../screen/atoms"
99
import { TimelineSelectorList } from "../screen/TimelineSelectorList"
1010
import { EntryListFooter } from "./EntryListFooter"
11-
import { useOnViewableItemsChanged } from "./hooks"
11+
import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
1212
import { ItemSeparatorFullWidth } from "./ItemSeparator"
1313
import { EntrySocialItem } from "./templates/EntrySocialItem"
1414

1515
export const EntryListContentSocial = forwardRef<
1616
ElementRef<typeof TimelineSelectorList>,
1717
{ entryIds: string[] | null; active?: boolean }
18-
>(({ entryIds, active }, ref) => {
18+
>(({ entryIds, active }, forwardRef) => {
1919
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage } =
2020
useFetchEntriesControls()
2121

22+
const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
23+
useImperativeHandle(forwardRef, () => ref.current!)
24+
// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-callback
2225
const renderItem = useCallback(
2326
({ item: id }: ListRenderItemInfo<string>) => <EntrySocialItem entryId={id} />,
2427
[],
@@ -31,6 +34,7 @@ export const EntryListContentSocial = forwardRef<
3134

3235
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
3336
disabled: active === false || isFetching,
37+
onScroll: hackOnScroll,
3438
})
3539

3640
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
@@ -51,6 +55,7 @@ export const EntryListContentSocial = forwardRef<
5155
onScroll={onScroll}
5256
ItemSeparatorComponent={ItemSeparatorFullWidth}
5357
ListFooterComponent={ListFooterComponent}
58+
style={hackStyle}
5459
/>
5560
)
5661
})

apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { useTypeScriptHappyCallback } from "@follow/hooks"
22
import type { MasonryFlashListProps } from "@shopify/flash-list"
33
import type { ElementRef } from "react"
4-
import { forwardRef, useMemo } from "react"
4+
import { forwardRef, useImperativeHandle, useMemo } from "react"
55
import { View } from "react-native"
66

77
import { useFetchEntriesControls } from "@/src/modules/screen/atoms"
88
import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
99

1010
import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList"
1111
import { GridEntryListFooter } from "./EntryListFooter"
12-
import { useOnViewableItemsChanged } from "./hooks"
12+
import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
1313
import { EntryVideoItem } from "./templates/EntryVideoItem"
1414

1515
export const EntryListContentVideo = forwardRef<
@@ -18,11 +18,14 @@ export const EntryListContentVideo = forwardRef<
1818
MasonryFlashListProps<string>,
1919
"data" | "renderItem"
2020
>
21-
>(({ entryIds, active, ...rest }, ref) => {
21+
>(({ entryIds, active, ...rest }, forwardRef) => {
22+
const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
23+
useImperativeHandle(forwardRef, () => ref.current!)
2224
const { fetchNextPage, refetch, isRefetching, isFetching, hasNextPage } =
2325
useFetchEntriesControls()
2426
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
2527
disabled: active === false || isFetching,
28+
onScroll: hackOnScroll,
2629
})
2730

2831
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
@@ -57,6 +60,7 @@ export const EntryListContentVideo = forwardRef<
5760
ListFooterComponent={ListFooterComponent}
5861
{...rest}
5962
onRefresh={refetch}
63+
style={hackStyle}
6064
/>
6165
)
6266
})

apps/mobile/src/modules/entry-list/hooks.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
1+
import type { FlashList } from "@shopify/flash-list"
12
import type ViewToken from "@shopify/flash-list/dist/viewability/ViewToken"
2-
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react"
3-
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
3+
import type { RefObject } from "react"
4+
import {
5+
useCallback,
6+
useContext,
7+
useEffect,
8+
useInsertionEffect,
9+
useMemo,
10+
useRef,
11+
useState,
12+
} from "react"
13+
import type { NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"
14+
import { useEventCallback } from "usehooks-ts"
415

516
import { useGeneralSettingKey } from "@/src/atoms/settings/general"
617
import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"
718
import { unreadSyncService } from "@/src/store/unread/store"
819

20+
import { PagerListVisibleContext, PagerListWillVisibleContext } from "../screen/PagerListContext"
21+
922
const defaultIdExtractor = (item: ViewToken) => item.key
1023
export function useOnViewableItemsChanged({
1124
disabled,
1225
idExtractor = defaultIdExtractor,
26+
onScroll: onScrollProp,
1327
}: {
1428
disabled?: boolean
1529
idExtractor?: (item: ViewToken) => string
30+
onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
1631
} = {}) {
1732
const orientation = useRef<"down" | "up">("down")
1833
const lastOffset = useRef(0)
@@ -82,12 +97,16 @@ export function useOnViewableItemsChanged({
8297
stableIdExtractor,
8398
])
8499

85-
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
86-
const currentOffset = e.nativeEvent.contentOffset.y
87-
const currentOrientation = currentOffset > lastOffset.current ? "down" : "up"
88-
orientation.current = currentOrientation
89-
lastOffset.current = currentOffset
90-
}, [])
100+
const onScroll = useCallback(
101+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
102+
const currentOffset = e.nativeEvent.contentOffset.y
103+
const currentOrientation = currentOffset > lastOffset.current ? "down" : "up"
104+
orientation.current = currentOrientation
105+
lastOffset.current = currentOffset
106+
onScrollProp?.(e)
107+
},
108+
[onScrollProp],
109+
)
91110

92111
return useMemo(
93112
() => ({ onViewableItemsChanged, onScroll, viewableItems }),
@@ -108,3 +127,33 @@ function useNonReactiveCallback<T extends (...args: any[]) => any>(fn: T): T {
108127
[ref],
109128
) as unknown as T
110129
}
130+
131+
export const usePagerListPerformanceHack = (provideRef?: RefObject<FlashList<any>>) => {
132+
const lastY = useRef(0)
133+
134+
const onScroll = useEventCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
135+
if (!visible) return
136+
137+
lastY.current = e.nativeEvent.contentOffset.y
138+
})
139+
140+
const visible = useContext(PagerListVisibleContext)
141+
const willVisible = useContext(PagerListWillVisibleContext)
142+
143+
const nextVisible = visible || willVisible
144+
145+
const ref = useRef<FlashList<any>>(null)
146+
147+
const usingRef = provideRef ?? ref
148+
const [style, setStyle] = useState<StyleProp<ViewStyle>>({})
149+
useEffect(() => {
150+
setStyle({ display: nextVisible ? "flex" : "none" })
151+
if (nextVisible && lastY.current > 0) {
152+
requestAnimationFrame(() => {
153+
usingRef.current?.scrollToOffset({ offset: lastY.current, animated: false })
154+
})
155+
}
156+
}, [nextVisible, usingRef])
157+
158+
return { onScroll, ref, style }
159+
}

0 commit comments

Comments
 (0)