perf(profile): improve rendering performance and selector efficiency
Some checks failed
Frontend CI / ota-android (push) Successful in 1m35s
Frontend CI / ota-ios (push) Successful in 1m34s
Frontend CI / build-and-push-web (push) Successful in 2m48s
Frontend CI / build-android-apk (push) Failing after 12m16s

Optimize profile-related screens and components by implementing memoization and granular Zustand selectors to reduce unnecessary re-renders.

- **Performance Optimization**:
  - Wrap `TabBar` in `React.memo` to prevent re-renders when parent components update.
  - Refactor `UserProfileScreen` to use a memoized `ProfileListHeader` component, decoupling the user header from tab state changes.
- **State Management**:
  - Replace object destructuring from `useAuthStore` with granular selectors (e.g., `useAuthStore((s) => s.logout)`) in `AccountDeletionScreen`, `EditProfileScreen`, `FollowListScreen`, and `SettingsScreen` to prevent re-renders on unrelated store changes.
- **Code Cleanup**:
  - Remove redundant case logic in `useUserProfile` hook.
This commit is contained in:
2026-05-18 01:06:46 +08:00
parent 4fde3e403a
commit 7a17323e8d
7 changed files with 102 additions and 60 deletions

View File

@@ -330,4 +330,4 @@ const TabBar: React.FC<TabBarProps> = ({
);
};
export default TabBar;
export default React.memo(TabBar);

View File

@@ -180,7 +180,7 @@ export const AccountDeletionScreen: React.FC = () => {
const router = useRouter();
const { isMobile } = useResponsive();
const insets = useSafeAreaInsets();
const { logout } = useAuthStore();
const logout = useAuthStore((s) => s.logout);
const [status, setStatus] = useState<DeletionStatusDTO | null>(null);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);

View File

@@ -279,7 +279,8 @@ export const EditProfileScreen: React.FC = () => {
const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
const navigation = useNavigation();
const router = useRouter();
const { currentUser, updateUser } = useAuthStore();
const currentUser = useAuthStore((s) => s.currentUser);
const updateUser = useAuthStore((s) => s.updateUser);
const { isWideScreen, isMobile, width } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
const insets = useSafeAreaInsets();

View File

@@ -32,7 +32,7 @@ const FollowListScreen: React.FC = () => {
const { userId = '', type: typeParam } = useLocalSearchParams<{ userId?: string; type?: string }>();
const type = typeParam === 'followers' ? 'followers' : 'following';
const { currentUser } = useAuthStore();
const currentUser = useAuthStore((s) => s.currentUser);
const { followUser, unfollowUser } = useUserStore.getState();
// 响应式布局

View File

@@ -218,7 +218,7 @@ function createSettingsStyles(colors: AppColors) {
export const SettingsScreen: React.FC = () => {
const router = useRouter();
const { logout } = useAuthStore();
const logout = useAuthStore((s) => s.logout);
const insets = useSafeAreaInsets();
const { isMobile } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });

View File

@@ -5,7 +5,7 @@
* 支持桌面端双栏布局
*/
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import {
View,
RefreshControl,
@@ -20,6 +20,67 @@ import { Loading, EmptyState, ResponsiveContainer } from '../../components/commo
import { useResponsive } from '../../hooks';
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
// 稳定的头部组件:切换 tab 时不会导致 UserProfileHeader 重渲染
// 因为 activeTab 只影响 TabBarUserProfileHeader 的 props 不变
interface ProfileListHeaderProps {
user: any;
isCurrentUser: boolean;
isBlocked: boolean;
handleFollow: () => void;
handleSettings?: () => void;
handleEditProfile?: () => void;
handleMessage?: () => Promise<void>;
handleBlock?: () => Promise<void>;
handleFollowingPress: () => void;
handleFollowersPress: () => void;
activeTab: number;
setActiveTab: (tab: number) => void;
tabBarContainerStyle: any;
}
const ProfileListHeader = React.memo(function ProfileListHeader({
user,
isCurrentUser,
isBlocked,
handleFollow,
handleSettings,
handleEditProfile,
handleMessage,
handleBlock,
handleFollowingPress,
handleFollowersPress,
activeTab,
setActiveTab,
tabBarContainerStyle,
}: ProfileListHeaderProps) {
if (!user) return null;
return (
<>
<UserProfileHeader
user={user}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
onFollow={handleFollow}
onSettings={handleSettings}
onEditProfile={handleEditProfile}
onMessage={handleMessage}
onBlock={handleBlock}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
<View style={tabBarContainerStyle}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
</>
);
});
interface UserProfileScreenProps {
mode: ProfileMode;
userId?: string;
@@ -110,50 +171,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
);
}, [loading, activeTab, mode, isBlockedProfile]);
// 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染)
const renderUserHeader = useMemo(() => {
if (!user) return null;
return (
<UserProfileHeader
user={user}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
onFollow={handleFollow}
onSettings={handleSettings}
onEditProfile={handleEditProfile}
onMessage={handleMessage}
onBlock={handleBlock}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
);
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
// 渲染 TabBar
const renderTabBar = useMemo(() => (
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
), [activeTab, setActiveTab, sharedStyles.tabBarContainer]);
// 渲染 FlashList 头部(用户信息 + TabBar
const renderListHeader = useCallback(() => {
if (!user) return null;
return (
<>
{renderUserHeader}
{renderTabBar}
</>
);
}, [user, renderUserHeader, renderTabBar]);
// 未登录/用户不存在状态
// 渲染帖子项/用户不存在状态
if (mode === 'self' && !currentUser) {
return (
<SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
@@ -200,7 +218,21 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
/>
}
>
{renderListHeader()}
<ProfileListHeader
user={user}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
handleFollow={handleFollow}
handleSettings={handleSettings}
handleEditProfile={handleEditProfile}
handleMessage={handleMessage}
handleBlock={handleBlock}
handleFollowingPress={handleFollowingPress}
handleFollowersPress={handleFollowersPress}
activeTab={activeTab}
setActiveTab={setActiveTab}
tabBarContainerStyle={sharedStyles.tabBarContainer}
/>
</ScrollView>
</View>
@@ -248,7 +280,23 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
data={currentPosts}
renderItem={renderPostItem}
keyExtractor={postKeyExtractor}
ListHeaderComponent={renderListHeader}
ListHeaderComponent={
<ProfileListHeader
user={user}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
handleFollow={handleFollow}
handleSettings={handleSettings}
handleEditProfile={handleEditProfile}
handleMessage={handleMessage}
handleBlock={handleBlock}
handleFollowingPress={handleFollowingPress}
handleFollowersPress={handleFollowersPress}
activeTab={activeTab}
setActiveTab={setActiveTab}
tabBarContainerStyle={sharedStyles.tabBarContainer}
/>
}
ListEmptyComponent={renderEmptyPosts}
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
showsVerticalScrollIndicator={false}

View File

@@ -315,7 +315,7 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
handleUserPress(post.author.id);
}
break;
case 'like':
case 'like':
case 'unlike':
post.is_liked ? postSyncService.unlikePost(post.id) : postSyncService.likePost(post.id);
break;
@@ -323,13 +323,6 @@ case 'like':
handlePostPress(post.id, true);
break;
case 'bookmark':
case 'unbookmark':
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
break;
case 'comment':
handlePostPress(post.id, true);
break;
case 'bookmark':
case 'unbookmark':
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
break;