From 98493c1501a69a0681ad9f1e4c4fdbcffaa16fae Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Mon, 29 Dec 2025 15:09:48 -0800 Subject: [PATCH] Lazy load mobile screens --- .../src/screens/app-screen/AppScreen.tsx | 49 ++++- .../src/screens/app-screen/AppTabScreen.tsx | 184 ++++++++++++++---- .../src/screens/app-screen/AppTabsScreen.tsx | 29 ++- .../screens/app-screen/ExploreTabScreen.tsx | 18 +- .../src/screens/root-screen/RootScreen.tsx | 27 ++- packages/mobile/src/utils/lazyScreen.tsx | 87 +++++++++ 6 files changed, 330 insertions(+), 64 deletions(-) create mode 100644 packages/mobile/src/utils/lazyScreen.tsx diff --git a/packages/mobile/src/screens/app-screen/AppScreen.tsx b/packages/mobile/src/screens/app-screen/AppScreen.tsx index 8b861015220..571d8aef7f7 100644 --- a/packages/mobile/src/screens/app-screen/AppScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppScreen.tsx @@ -5,19 +5,48 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack' import { Platform } from 'react-native' import { setLastNavAction } from 'app/hooks/useNavigation' - -import { BuySellModalScreen } from '../buy-sell-screen' -import { ChangePasswordModalScreen } from '../change-password-screen' -import { CreateChatBlastNavigator } from '../create-chat-blast-screen/CreateChatBlastNavigator' -import { EditCollectionScreen } from '../edit-collection-screen' -import { EditTrackModalScreen } from '../edit-track-screen' -import { ExternalWalletsModalScreen } from '../external-wallets' -import { FeatureFlagOverrideScreen } from '../feature-flag-override-screen' -import { TipArtistModalScreen } from '../tip-artist-screen' -import { UploadModalScreen } from '../upload-screen' +import { lazyScreenNamed } from 'app/utils/lazyScreen' import { AppTabsScreen } from './AppTabsScreen' +// Lazy load modal screens +const BuySellModalScreen = lazyScreenNamed( + () => import('../buy-sell-screen'), + 'BuySellModalScreen' +) +const ChangePasswordModalScreen = lazyScreenNamed( + () => import('../change-password-screen'), + 'ChangePasswordModalScreen' +) +const CreateChatBlastNavigator = lazyScreenNamed( + () => import('../create-chat-blast-screen/CreateChatBlastNavigator'), + 'CreateChatBlastNavigator' +) +const EditCollectionScreen = lazyScreenNamed( + () => import('../edit-collection-screen'), + 'EditCollectionScreen' +) +const EditTrackModalScreen = lazyScreenNamed( + () => import('../edit-track-screen'), + 'EditTrackModalScreen' +) +const ExternalWalletsModalScreen = lazyScreenNamed( + () => import('../external-wallets'), + 'ExternalWalletsModalScreen' +) +const FeatureFlagOverrideScreen = lazyScreenNamed( + () => import('../feature-flag-override-screen'), + 'FeatureFlagOverrideScreen' +) +const TipArtistModalScreen = lazyScreenNamed( + () => import('../tip-artist-screen'), + 'TipArtistModalScreen' +) +const UploadModalScreen = lazyScreenNamed( + () => import('../upload-screen'), + 'UploadModalScreen' +) + const Stack = createNativeStackNavigator() export const AppScreen = () => { diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index 4aa82fcba17..ebb8baf0f0d 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -25,53 +25,155 @@ import type { FilterButtonScreenParams } from '@audius/harmony-native' import { useDrawer } from 'app/hooks/useDrawer' import { setLastNavAction } from 'app/hooks/useNavigation' import { AppDrawerContext } from 'app/screens/app-drawer-screen' -import { AudioScreen } from 'app/screens/audio-screen' -import { CashScreen } from 'app/screens/cash-screen' -import { ChangeEmailModalScreen } from 'app/screens/change-email-screen/ChangeEmailScreen' -import { ChatListScreen } from 'app/screens/chat-screen/ChatListScreen' -import { ChatScreen } from 'app/screens/chat-screen/ChatScreen' -import { ChatUserListScreen } from 'app/screens/chat-screen/ChatUserListScreen' -import { - CoinDetailsScreen, - EditCoinDetailsScreen, - ExclusiveTracksScreen -} from 'app/screens/coin-details-screen' -import { CoinRedeemScreen } from 'app/screens/coin-redeem-screen' -import { CollectionScreen } from 'app/screens/collection-screen/CollectionScreen' -import { EditProfileScreen } from 'app/screens/edit-profile-screen' -import { ProfileScreen } from 'app/screens/profile-screen' -import { RewardsScreen } from 'app/screens/rewards-screen' -import { - AboutScreen, - AccountSettingsScreen, - ListeningHistoryScreen, - DownloadSettingsScreen, - InboxSettingsScreen, - CommentSettingsScreen, - NotificationSettingsScreen, - SettingsScreen -} from 'app/screens/settings-screen' -import { TrackScreen } from 'app/screens/track-screen' -import { TrackRemixesScreen } from 'app/screens/track-screen/TrackRemixesScreen' -import { - FavoritedScreen, - FollowersScreen, - FollowingScreen, - RepostsScreen, - NotificationUsersScreen, - MutualsScreen, - RelatedArtistsScreen, - TopSupportersScreen, - SupportingUsersScreen, - CoinLeaderboardScreen -} from 'app/screens/user-list-screen' -import { WalletScreen } from 'app/screens/wallet-screen' +import { lazyScreenNamed } from 'app/utils/lazyScreen' import { ArtistCoinSortScreen } from '../artist-coin-sort-screen/ArtistCoinSortScreen' import { ArtistCoinsExploreScreen } from '../artist-coins-explore-screen/ArtistCoinsExploreScreen' import { useAppScreenOptions } from './useAppScreenOptions' +// Lazy load all screens +const AudioScreen = lazyScreenNamed( + () => import('app/screens/audio-screen'), + 'AudioScreen' +) +const CashScreen = lazyScreenNamed( + () => import('app/screens/cash-screen'), + 'CashScreen' +) +const ChangeEmailModalScreen = lazyScreenNamed( + () => import('app/screens/change-email-screen/ChangeEmailScreen'), + 'ChangeEmailModalScreen' +) +const ChatListScreen = lazyScreenNamed( + () => import('app/screens/chat-screen/ChatListScreen'), + 'ChatListScreen' +) +const ChatScreen = lazyScreenNamed( + () => import('app/screens/chat-screen/ChatScreen'), + 'ChatScreen' +) +const ChatUserListScreen = lazyScreenNamed( + () => import('app/screens/chat-screen/ChatUserListScreen'), + 'ChatUserListScreen' +) +const CoinDetailsScreen = lazyScreenNamed( + () => import('app/screens/coin-details-screen'), + 'CoinDetailsScreen' +) +const EditCoinDetailsScreen = lazyScreenNamed( + () => import('app/screens/coin-details-screen'), + 'EditCoinDetailsScreen' +) +const ExclusiveTracksScreen = lazyScreenNamed( + () => import('app/screens/coin-details-screen'), + 'ExclusiveTracksScreen' +) +const CoinRedeemScreen = lazyScreenNamed( + () => import('app/screens/coin-redeem-screen'), + 'CoinRedeemScreen' +) +const CollectionScreen = lazyScreenNamed( + () => import('app/screens/collection-screen/CollectionScreen'), + 'CollectionScreen' +) +const EditProfileScreen = lazyScreenNamed( + () => import('app/screens/edit-profile-screen'), + 'EditProfileScreen' +) +const ProfileScreen = lazyScreenNamed( + () => import('app/screens/profile-screen'), + 'ProfileScreen' +) +const RewardsScreen = lazyScreenNamed( + () => import('app/screens/rewards-screen'), + 'RewardsScreen' +) +const AboutScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'AboutScreen' +) +const AccountSettingsScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'AccountSettingsScreen' +) +const ListeningHistoryScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'ListeningHistoryScreen' +) +const DownloadSettingsScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'DownloadSettingsScreen' +) +const InboxSettingsScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'InboxSettingsScreen' +) +const CommentSettingsScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'CommentSettingsScreen' +) +const NotificationSettingsScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'NotificationSettingsScreen' +) +const SettingsScreen = lazyScreenNamed( + () => import('app/screens/settings-screen'), + 'SettingsScreen' +) +const TrackScreen = lazyScreenNamed( + () => import('app/screens/track-screen'), + 'TrackScreen' +) +const TrackRemixesScreen = lazyScreenNamed( + () => import('app/screens/track-screen/TrackRemixesScreen'), + 'TrackRemixesScreen' +) +const FavoritedScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'FavoritedScreen' +) +const FollowersScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'FollowersScreen' +) +const FollowingScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'FollowingScreen' +) +const RepostsScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'RepostsScreen' +) +const NotificationUsersScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'NotificationUsersScreen' +) +const MutualsScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'MutualsScreen' +) +const RelatedArtistsScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'RelatedArtistsScreen' +) +const TopSupportersScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'TopSupportersScreen' +) +const SupportingUsersScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'SupportingUsersScreen' +) +const CoinLeaderboardScreen = lazyScreenNamed( + () => import('app/screens/user-list-screen'), + 'CoinLeaderboardScreen' +) +const WalletScreen = lazyScreenNamed( + () => import('app/screens/wallet-screen'), + 'WalletScreen' +) + export type AppTabScreenParamList = { Track: { searchTrack?: SearchTrack diff --git a/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx index eeb89582a14..9f50ca1c3b5 100644 --- a/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabsScreen.tsx @@ -2,21 +2,40 @@ import type { BottomTabBarProps } from '@react-navigation/bottom-tabs' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import type { NavigatorScreenParams } from '@react-navigation/native' +import { lazyScreenNamed } from 'app/utils/lazyScreen' + import { usePhantomConnect } from '../external-wallets/usePhantomConnect' import { AppTabBar } from './AppTabBar' import type { ExploreTabScreenParamList } from './ExploreTabScreen' -import { ExploreTabScreen } from './ExploreTabScreen' import type { FavoritesTabScreenParamList } from './FavoritesTabScreen' -import { FavoritesTabScreen } from './FavoritesTabScreen' import type { FeedTabScreenParamList } from './FeedTabScreen' -import { FeedTabScreen } from './FeedTabScreen' -import { NotificationsTabScreen } from './NotificationsTabScreen' import type { ProfileTabScreenParamList } from './ProfileTabScreen' import type { TrendingTabScreenParamList } from './TrendingTabScreen' -import { TrendingTabScreen } from './TrendingTabScreen' import { usePrefetchNotifications } from './usePrefetchNotifications' +// Lazy load tab screens +const FeedTabScreen = lazyScreenNamed( + () => import('./FeedTabScreen'), + 'FeedTabScreen' +) +const TrendingTabScreen = lazyScreenNamed( + () => import('./TrendingTabScreen'), + 'TrendingTabScreen' +) +const ExploreTabScreen = lazyScreenNamed( + () => import('./ExploreTabScreen'), + 'ExploreTabScreen' +) +const FavoritesTabScreen = lazyScreenNamed( + () => import('./FavoritesTabScreen'), + 'FavoritesTabScreen' +) +const NotificationsTabScreen = lazyScreenNamed( + () => import('./NotificationsTabScreen'), + 'NotificationsTabScreen' +) + export type AppScreenParamList = { feed: NavigatorScreenParams trending: NavigatorScreenParams diff --git a/packages/mobile/src/screens/app-screen/ExploreTabScreen.tsx b/packages/mobile/src/screens/app-screen/ExploreTabScreen.tsx index 26f0ac1d292..d4f2997d023 100644 --- a/packages/mobile/src/screens/app-screen/ExploreTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/ExploreTabScreen.tsx @@ -1,12 +1,24 @@ import type { SearchCategory, SearchFilters } from '@audius/common/api' -import { SearchExploreScreen } from '../explore-screen/SearchExploreScreen' -import { TrendingPlaylistsScreen } from '../explore-screen/tabs/ForYouTab/TrendingPlaylistsScreen' -import { TrendingUndergroundScreen } from '../explore-screen/tabs/ForYouTab/TrendingUndergroundScreen' +import { lazyScreenNamed } from 'app/utils/lazyScreen' import type { AppTabScreenParamList } from './AppTabScreen' import { createAppTabScreenStack } from './createAppTabScreenStack' +// Lazy load nested explore screens +const SearchExploreScreen = lazyScreenNamed( + () => import('../explore-screen/SearchExploreScreen'), + 'SearchExploreScreen' +) +const TrendingPlaylistsScreen = lazyScreenNamed( + () => import('../explore-screen/tabs/ForYouTab/TrendingPlaylistsScreen'), + 'TrendingPlaylistsScreen' +) +const TrendingUndergroundScreen = lazyScreenNamed( + () => import('../explore-screen/tabs/ForYouTab/TrendingUndergroundScreen'), + 'TrendingUndergroundScreen' +) + export type ExploreTabScreenParamList = AppTabScreenParamList & { SearchExplore: { autoFocus?: boolean diff --git a/packages/mobile/src/screens/root-screen/RootScreen.tsx b/packages/mobile/src/screens/root-screen/RootScreen.tsx index a66d9ca9a02..d4ea398ae26 100644 --- a/packages/mobile/src/screens/root-screen/RootScreen.tsx +++ b/packages/mobile/src/screens/root-screen/RootScreen.tsx @@ -29,12 +29,29 @@ import { useDrawer } from 'app/hooks/useDrawer' import { useNavigation } from 'app/hooks/useNavigation' import { useUpdateRequired } from 'app/hooks/useUpdateRequired' import { SplashScreen } from 'app/screens/splash-screen' -import { UpdateRequiredScreen } from 'app/screens/update-required-screen' import { enterBackground, enterForeground } from 'app/store/lifecycle/actions' -import { AppDrawerScreen } from '../app-drawer-screen' -import { ResetPasswordModalScreen } from '../reset-password-screen' -import { SignOnStack } from '../sign-on-screen' +import { lazyScreenNamed } from 'app/utils/lazyScreen' + +// Lazy load root-level screens +const AppDrawerScreen = lazyScreenNamed( + () => import('../app-drawer-screen'), + 'AppDrawerScreen' +) +const ResetPasswordModalScreen = lazyScreenNamed( + () => import('../reset-password-screen'), + 'ResetPasswordModalScreen' +) +const UpdateRequiredScreen = lazyScreenNamed( + () => import('../update-required-screen'), + 'UpdateRequiredScreen' +) + +// SignOnStack needs special handling since it's used as a render function with props +const SignOnStackLazy = lazyScreenNamed( + () => import('../sign-on-screen'), + 'SignOnStack' +) import { StatusBar } from './StatusBar' import { useResetNotificationBadgeCount } from './useResetNotificationBadgeCount' @@ -168,7 +185,7 @@ export const RootScreen = () => { ) : ( {() => ( - )} diff --git a/packages/mobile/src/utils/lazyScreen.tsx b/packages/mobile/src/utils/lazyScreen.tsx new file mode 100644 index 00000000000..caec588235e --- /dev/null +++ b/packages/mobile/src/utils/lazyScreen.tsx @@ -0,0 +1,87 @@ +import React, { Suspense } from 'react' +import { ActivityIndicator, View } from 'react-native' + +/** + * Creates a lazy-loaded screen component for React Navigation. + * This wrapper handles Suspense boundaries and provides a loading fallback. + * + * @param importFn - Function that returns a dynamic import promise + * @param fallback - Optional custom fallback component (defaults to ActivityIndicator) + * @returns A component that can be used with React Navigation's Stack.Screen + * + * @example + * const LazyProfileScreen = lazyScreen(() => import('app/screens/profile-screen')) + * // Then use: + */ +export const lazyScreen = >( + importFn: () => Promise<{ default: T } | { [key: string]: T }>, + fallback?: React.ComponentType +): React.ComponentType => { + const LazyComponent = React.lazy(async () => { + const module = await importFn() + // Handle both default exports and named exports + if ('default' in module) { + return module + } + // If no default, try to find the first exported component + const firstExport = Object.values(module)[0] + return { default: firstExport as T } + }) + + const FallbackComponent = fallback ?? (() => ( + + + + )) + + return (props: any) => ( + }> + + + ) +} + +/** + * Helper to create lazy screens with named exports. + * Use this when the screen is exported as a named export rather than default. + * + * @param importFn - Function that returns a dynamic import promise + * @param exportName - Name of the exported component + * @param fallback - Optional custom fallback component + * @returns A component that can be used with React Navigation's Stack.Screen + * + * @example + * const LazyProfileScreen = lazyScreenNamed( + * () => import('app/screens/profile-screen'), + * 'ProfileScreen' + * ) + */ +export const lazyScreenNamed = >( + importFn: () => Promise<{ [key: string]: T }>, + exportName: string, + fallback?: React.ComponentType +): React.ComponentType => { + const LazyComponent = React.lazy(async () => { + const module = await importFn() + const component = module[exportName] + if (!component) { + throw new Error( + `Export "${exportName}" not found in module. Available exports: ${Object.keys(module).join(', ')}` + ) + } + return { default: component } + }) + + const FallbackComponent = fallback ?? (() => ( + + + + )) + + return (props: any) => ( + }> + + + ) +} +