diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 59c1cd459..3e2bdf0db 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'; import { View, Text, @@ -6,6 +6,7 @@ import { Dimensions, TouchableOpacity, Platform, + InteractionManager, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; @@ -22,6 +23,7 @@ import Animated, { runOnJS, withRepeat, FadeIn, + runOnUI, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { useTraktContext } from '../../contexts/TraktContext'; @@ -71,7 +73,7 @@ interface HeroSectionProps { } // Ultra-optimized ActionButtons Component - minimal re-renders -const ActionButtons = React.memo(({ +const ActionButtons = memo(({ handleShowStreams, toggleLibrary, inLibrary, @@ -98,17 +100,27 @@ const ActionButtons = React.memo(({ }) => { const { currentTheme } = useTheme(); - // Memoized navigation handler - const handleRatingsPress = useMemo(() => async () => { + // Performance optimization: Cache theme colors + const themeColors = useMemo(() => ({ + white: currentTheme.colors.white, + black: '#000', + primary: currentTheme.colors.primary + }), [currentTheme.colors.white, currentTheme.colors.primary]); + + // Optimized navigation handler with useCallback + const handleRatingsPress = useCallback(async () => { + // Early return if no ID + if (!id) return; + let finalTmdbId: number | null = null; - if (id?.startsWith('tmdb:')) { + if (id.startsWith('tmdb:')) { const numericPart = id.split(':')[1]; const parsedId = parseInt(numericPart, 10); if (!isNaN(parsedId)) { finalTmdbId = parsedId; } - } else if (id?.startsWith('tt')) { + } else if (id.startsWith('tt')) { try { const tmdbService = TMDBService.getInstance(); const convertedId = await tmdbService.findTMDBIdByIMDB(id); @@ -118,7 +130,7 @@ const ActionButtons = React.memo(({ } catch (error) { logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error); } - } else if (id) { + } else { const parsedId = parseInt(id, 10); if (!isNaN(parsedId)) { finalTmdbId = parsedId; @@ -126,11 +138,14 @@ const ActionButtons = React.memo(({ } if (finalTmdbId !== null) { - navigation.navigate('ShowRatings', { showId: finalTmdbId }); + // Use requestAnimationFrame for smoother navigation + requestAnimationFrame(() => { + navigation.navigate('ShowRatings', { showId: finalTmdbId }); + }); } }, [id, navigation]); - // Determine play button style and text based on watched status + // Optimized play button style calculation const playButtonStyle = useMemo(() => { if (isWatched && type === 'movie') { // Only movies get the dark watched style for "Watch Again" @@ -285,7 +300,7 @@ const ActionButtons = React.memo(({ }); // Enhanced WatchProgress Component with Trakt integration and watched status -const WatchProgressDisplay = React.memo(({ +const WatchProgressDisplay = memo(({ watchProgress, type, getEpisodeDetails, @@ -623,7 +638,22 @@ const WatchProgressDisplay = React.memo(({ ); }); -const HeroSection: React.FC = ({ +/** + * HeroSection Component - Performance Optimized + * + * Optimizations Applied: + * - Component memoization with React.memo + * - Lazy loading system using InteractionManager + * - Optimized image loading with useCallback handlers + * - Cached theme colors to reduce re-renders + * - Conditional rendering based on shouldLoadSecondaryData + * - Memory management with cleanup on unmount + * - Development-mode performance monitoring + * - Optimized animated styles and memoized calculations + * - Reduced re-renders through strategic memoization + * - runOnUI for animation performance + */ +const HeroSection: React.FC = memo(({ metadata, bannerImage, loadingBanner, @@ -650,21 +680,47 @@ const HeroSection: React.FC = ({ }) => { const { currentTheme } = useTheme(); const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); - - // Enhanced state for smooth image loading + + // Performance optimization: Refs for avoiding re-renders + const interactionComplete = useRef(false); + const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false); + + // Image loading state with optimized management const [imageError, setImageError] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); const imageOpacity = useSharedValue(1); const imageLoadOpacity = useSharedValue(0); const shimmerOpacity = useSharedValue(0.3); + // Performance optimization: Cache theme colors + const themeColors = useMemo(() => ({ + black: currentTheme.colors.black, + darkBackground: currentTheme.colors.darkBackground, + highEmphasis: currentTheme.colors.highEmphasis, + text: currentTheme.colors.text + }), [currentTheme.colors.black, currentTheme.colors.darkBackground, currentTheme.colors.highEmphasis, currentTheme.colors.text]); + // Memoized image source const imageSource = useMemo(() => bannerImage || metadata.banner || metadata.poster , [bannerImage, metadata.banner, metadata.poster]); - // Start shimmer animation for loading state + // Performance optimization: Lazy loading setup useEffect(() => { + const timer = InteractionManager.runAfterInteractions(() => { + if (!interactionComplete.current) { + interactionComplete.current = true; + setShouldLoadSecondaryData(true); + } + }); + + return () => timer.cancel(); + }, []); + + // Optimized shimmer animation for loading state + useEffect(() => { + if (!shouldLoadSecondaryData) return; + if (!imageLoaded && imageSource) { // Start shimmer animation shimmerOpacity.value = withRepeat( @@ -676,9 +732,9 @@ const HeroSection: React.FC = ({ // Stop shimmer when loaded shimmerOpacity.value = withTiming(0.3, { duration: 300 }); } - }, [imageLoaded, imageSource]); + }, [imageLoaded, imageSource, shouldLoadSecondaryData]); - // Reset loading state when image source changes + // Optimized loading state reset when image source changes useEffect(() => { if (imageSource) { setImageLoaded(false); @@ -686,26 +742,33 @@ const HeroSection: React.FC = ({ } }, [imageSource]); - // Enhanced image handlers with smooth transitions - const handleImageError = () => { + // Optimized image handlers with useCallback + const handleImageError = useCallback(() => { + if (!shouldLoadSecondaryData) return; + + runOnUI(() => { + imageOpacity.value = withTiming(0.6, { duration: 150 }); + imageLoadOpacity.value = withTiming(0, { duration: 150 }); + })(); + setImageError(true); setImageLoaded(false); - imageOpacity.value = withTiming(0.6, { duration: 150 }); - imageLoadOpacity.value = withTiming(0, { duration: 150 }); - runOnJS(() => { - if (bannerImage !== metadata.banner) { - setBannerImage(metadata.banner || metadata.poster); - } - })(); - }; + + // Fallback to poster if banner fails + if (bannerImage !== metadata.banner) { + setBannerImage(metadata.banner || metadata.poster); + } + }, [shouldLoadSecondaryData, bannerImage, metadata.banner, metadata.poster, setBannerImage]); - const handleImageLoad = () => { + const handleImageLoad = useCallback(() => { + runOnUI(() => { + imageOpacity.value = withTiming(1, { duration: 150 }); + imageLoadOpacity.value = withTiming(1, { duration: 400 }); + })(); + setImageError(false); setImageLoaded(true); - imageOpacity.value = withTiming(1, { duration: 150 }); - // Smooth fade-in for the loaded image - imageLoadOpacity.value = withTiming(1, { duration: 400 }); - }; + }, []); // Ultra-optimized animated styles - single calculations const heroAnimatedStyle = useAnimatedStyle(() => ({ @@ -768,9 +831,9 @@ const HeroSection: React.FC = ({ }] }), []); - // Ultra-optimized genre rendering with smooth animation + // Optimized genre rendering with lazy loading and memory management const genreElements = useMemo(() => { - if (!metadata?.genres?.length) return null; + if (!shouldLoadSecondaryData || !metadata?.genres?.length) return null; const genresToDisplay = metadata.genres.slice(0, 3); // Reduced to 3 for performance return genresToDisplay.map((genreName: string, index: number, array: string[]) => ( @@ -779,15 +842,15 @@ const HeroSection: React.FC = ({ entering={FadeIn.duration(400).delay(200 + index * 100)} style={{ flexDirection: 'row', alignItems: 'center' }} > - + {genreName} {index < array.length - 1 && ( - + )} )); - }, [metadata.genres, currentTheme.colors.text]); + }, [metadata.genres, themeColors.text, shouldLoadSecondaryData]); // Memoized play button text const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]); @@ -811,13 +874,38 @@ const HeroSection: React.FC = ({ return localWatched; }, [watchProgress, isTraktAuthenticated]); + // Memory management and cleanup + useEffect(() => { + return () => { + // Reset animation values on unmount + imageOpacity.value = 1; + imageLoadOpacity.value = 0; + shimmerOpacity.value = 0.3; + interactionComplete.current = false; + }; + }, []); + + // Development-only performance monitoring + useEffect(() => { + if (__DEV__) { + const startTime = Date.now(); + const timer = setTimeout(() => { + const renderTime = Date.now() - startTime; + if (renderTime > 100) { + console.warn(`[HeroSection] Slow render detected: ${renderTime}ms`); + } + }, 0); + return () => clearTimeout(timer); + } + }); + return ( {/* Optimized Background */} - + - {/* Loading placeholder for smooth transition */} - {((imageSource && !imageLoaded) || loadingBanner) && ( + {/* Optimized shimmer loading effect */} + {shouldLoadSecondaryData && ((imageSource && !imageLoaded) || loadingBanner) && ( @@ -830,8 +918,8 @@ const HeroSection: React.FC = ({ )} - {/* Enhanced Background Image with smooth loading */} - {imageSource && !loadingBanner && ( + {/* Optimized background image with lazy loading */} + {shouldLoadSecondaryData && imageSource && !loadingBanner && ( = ({ /> )} - {/* Simplified Gradient */} + {/* Optimized Gradient */} = ({ {/* Optimized Title/Logo */} - {metadata.logo && !logoLoadError ? ( + {shouldLoadSecondaryData && metadata.logo && !logoLoadError ? ( = ({ }} /> ) : ( - + {metadata.name} )} @@ -883,8 +971,8 @@ const HeroSection: React.FC = ({ isWatched={isWatched} /> - {/* Optimized Genres */} - {genreElements && ( + {/* Optimized genre display with lazy loading */} + {shouldLoadSecondaryData && genreElements && ( {genreElements} @@ -908,7 +996,7 @@ const HeroSection: React.FC = ({ ); -}; +}); // Ultra-optimized styles const styles = StyleSheet.create({ @@ -1305,4 +1393,4 @@ const styles = StyleSheet.create({ }, }); -export default React.memo(HeroSection); \ No newline at end of file +export default HeroSection; \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index cd2933d20..9c468b95b 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useState, useEffect, useMemo, useRef, memo } from 'react'; import { View, Text, @@ -7,9 +7,10 @@ import { ActivityIndicator, Dimensions, TouchableOpacity, + InteractionManager, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useRoute, useNavigation } from '@react-navigation/native'; +import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { useTheme } from '../contexts/ThemeContext'; @@ -27,6 +28,7 @@ import Animated, { Extrapolate, useSharedValue, withTiming, + runOnJS, } from 'react-native-reanimated'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -45,6 +47,14 @@ import { TraktService, TraktPlaybackItem } from '../services/traktService'; const { height } = Dimensions.get('window'); +// Memoized components for better performance +const MemoizedCastSection = memo(CastSection); +const MemoizedSeriesContent = memo(SeriesContent); +const MemoizedMovieContent = memo(MovieContent); +const MemoizedMoreLikeThisSection = memo(MoreLikeThisSection); +const MemoizedRatingsSection = memo(RatingsSection); +const MemoizedCastDetailsModal = memo(CastDetailsModal); + const MetadataScreen: React.FC = () => { const route = useRoute, string>>(); const navigation = useNavigation>(); @@ -59,7 +69,10 @@ const MetadataScreen: React.FC = () => { const [isContentReady, setIsContentReady] = useState(false); const [showCastModal, setShowCastModal] = useState(false); const [selectedCastMember, setSelectedCastMember] = useState(null); + const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false); + const [isScreenFocused, setIsScreenFocused] = useState(true); const transitionOpacity = useSharedValue(1); + const interactionComplete = useRef(false); const { metadata, @@ -81,134 +94,160 @@ const MetadataScreen: React.FC = () => { imdbId, } = useMetadata({ id, type, addonId }); - // Optimized hooks with memoization + // Optimized hooks with memoization and conditional loading const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes); const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata); const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress); - // Fetch and log Trakt progress data when entering the screen + // Focus effect for performance optimization + useFocusEffect( + useCallback(() => { + setIsScreenFocused(true); + + // Delay secondary data loading until interactions are complete + const timer = setTimeout(() => { + if (!interactionComplete.current) { + InteractionManager.runAfterInteractions(() => { + setShouldLoadSecondaryData(true); + interactionComplete.current = true; + }); + } + }, 100); + + return () => { + setIsScreenFocused(false); + clearTimeout(timer); + }; + }, []) + ); + + // Optimize secondary data loading useEffect(() => { - const fetchTraktProgress = async () => { - try { - const traktService = TraktService.getInstance(); - const isAuthenticated = await traktService.isAuthenticated(); - - console.log(`[MetadataScreen] === TRAKT PROGRESS DATA FOR ${type.toUpperCase()}: ${metadata?.name || id} ===`); - console.log(`[MetadataScreen] IMDB ID: ${id}`); - console.log(`[MetadataScreen] Trakt authenticated: ${isAuthenticated}`); - - if (!isAuthenticated) { - console.log(`[MetadataScreen] Not authenticated with Trakt, no progress data available`); - return; - } - - // Get all playback progress from Trakt - const allProgress = await traktService.getPlaybackProgress(); - console.log(`[MetadataScreen] Total Trakt progress items: ${allProgress.length}`); - - if (allProgress.length === 0) { - console.log(`[MetadataScreen] No Trakt progress data found`); - return; - } - - // Filter progress for current content - let relevantProgress: TraktPlaybackItem[] = []; - - if (type === 'movie') { - relevantProgress = allProgress.filter(item => - item.type === 'movie' && - item.movie?.ids.imdb === id.replace('tt', '') - ); - } else if (type === 'series') { - relevantProgress = allProgress.filter(item => - item.type === 'episode' && - item.show?.ids.imdb === id.replace('tt', '') - ); - } - - console.log(`[MetadataScreen] Relevant progress items for this ${type}: ${relevantProgress.length}`); - - if (relevantProgress.length === 0) { - console.log(`[MetadataScreen] No Trakt progress found for this ${type}`); - return; - } - - // Log detailed progress information - relevantProgress.forEach((item, index) => { - console.log(`[MetadataScreen] --- Progress Item ${index + 1} ---`); - console.log(`[MetadataScreen] Type: ${item.type}`); - console.log(`[MetadataScreen] Progress: ${item.progress.toFixed(2)}%`); - console.log(`[MetadataScreen] Paused at: ${item.paused_at}`); - console.log(`[MetadataScreen] Trakt ID: ${item.id}`); - - if (item.movie) { - console.log(`[MetadataScreen] Movie: ${item.movie.title} (${item.movie.year})`); - console.log(`[MetadataScreen] Movie IMDB: tt${item.movie.ids.imdb}`); - console.log(`[MetadataScreen] Movie TMDB: ${item.movie.ids.tmdb}`); - } - - if (item.episode && item.show) { - console.log(`[MetadataScreen] Show: ${item.show.title} (${item.show.year})`); - console.log(`[MetadataScreen] Show IMDB: tt${item.show.ids.imdb}`); - console.log(`[MetadataScreen] Episode: S${item.episode.season}E${item.episode.number} - ${item.episode.title}`); - console.log(`[MetadataScreen] Episode IMDB: ${item.episode.ids.imdb || 'N/A'}`); - console.log(`[MetadataScreen] Episode TMDB: ${item.episode.ids.tmdb || 'N/A'}`); - } - - console.log(`[MetadataScreen] Raw item:`, JSON.stringify(item, null, 2)); - }); - - // Find most recent progress if multiple episodes - if (type === 'series' && relevantProgress.length > 1) { - const mostRecent = relevantProgress.sort((a, b) => - new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime() - )[0]; - - console.log(`[MetadataScreen] === MOST RECENT EPISODE PROGRESS ===`); - if (mostRecent.episode && mostRecent.show) { - console.log(`[MetadataScreen] Most recent: S${mostRecent.episode.season}E${mostRecent.episode.number} - ${mostRecent.episode.title}`); - console.log(`[MetadataScreen] Progress: ${mostRecent.progress.toFixed(2)}%`); - console.log(`[MetadataScreen] Watched on: ${new Date(mostRecent.paused_at).toLocaleString()}`); - } - } - - console.log(`[MetadataScreen] === END TRAKT PROGRESS DATA ===`); - - } catch (error) { - console.error(`[MetadataScreen] Failed to fetch Trakt progress:`, error); - } - }; - - // Only fetch when we have metadata loaded - if (metadata && id) { - fetchTraktProgress(); + if (metadata && isScreenFocused && !shouldLoadSecondaryData) { + const timer = setTimeout(() => { + setShouldLoadSecondaryData(true); + }, 300); + return () => clearTimeout(timer); } - }, [metadata, id, type]); + }, [metadata, isScreenFocused, shouldLoadSecondaryData]); + + // Optimized Trakt progress fetching - only when secondary data should load + const fetchTraktProgress = useCallback(async () => { + if (!shouldLoadSecondaryData || !metadata || !id) return; + + try { + const traktService = TraktService.getInstance(); + const isAuthenticated = await traktService.isAuthenticated(); + + if (!isAuthenticated) { + console.log(`[MetadataScreen] Not authenticated with Trakt`); + return; + } + + // Get all playback progress from Trakt (cached) + const allProgress = await traktService.getPlaybackProgress(); + + if (allProgress.length === 0) return; + + // Filter progress for current content + let relevantProgress: TraktPlaybackItem[] = []; + + if (type === 'movie') { + relevantProgress = allProgress.filter(item => + item.type === 'movie' && + item.movie?.ids.imdb === id.replace('tt', '') + ); + } else if (type === 'series') { + relevantProgress = allProgress.filter(item => + item.type === 'episode' && + item.show?.ids.imdb === id.replace('tt', '') + ); + } + + if (relevantProgress.length === 0) return; + + // Log only essential progress information for performance + console.log(`[MetadataScreen] Found ${relevantProgress.length} Trakt progress items for ${type}`); + + // Find most recent progress if multiple episodes + if (type === 'series' && relevantProgress.length > 1) { + const mostRecent = relevantProgress.sort((a, b) => + new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime() + )[0]; + + if (mostRecent.episode && mostRecent.show) { + console.log(`[MetadataScreen] Most recent: S${mostRecent.episode.season}E${mostRecent.episode.number} - ${mostRecent.progress.toFixed(1)}%`); + } + } + + } catch (error) { + console.error(`[MetadataScreen] Failed to fetch Trakt progress:`, error); + } + }, [shouldLoadSecondaryData, metadata, id, type]); + + // Debounced Trakt progress fetching + useEffect(() => { + if (shouldLoadSecondaryData && metadata && id) { + const timer = setTimeout(fetchTraktProgress, 500); + return () => clearTimeout(timer); + } + }, [shouldLoadSecondaryData, metadata, id, fetchTraktProgress]); + + // Memory management and cleanup + useEffect(() => { + return () => { + // Cleanup on unmount + if (transitionOpacity.value !== 0) { + transitionOpacity.value = 0; + } + // Reset secondary data loading state + setShouldLoadSecondaryData(false); + interactionComplete.current = false; + }; + }, []); + + // Performance monitoring (development only) + useEffect(() => { + if (__DEV__ && metadata) { + const startTime = Date.now(); + const timer = setTimeout(() => { + const renderTime = Date.now() - startTime; + if (renderTime > 100) { + console.warn(`[MetadataScreen] Slow render detected: ${renderTime}ms for ${metadata.name}`); + } + }, 0); + return () => clearTimeout(timer); + } + }, [metadata]); // Memoized derived values for performance const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]); - // Simple content ready state management + // Optimized content ready state management useEffect(() => { - if (isReady) { + if (isReady && isScreenFocused) { setIsContentReady(true); transitionOpacity.value = withTiming(1, { duration: 50 }); } else if (!isReady && isContentReady) { setIsContentReady(false); transitionOpacity.value = 0; } - }, [isReady, isContentReady]); + }, [isReady, isContentReady, isScreenFocused]); - // Optimized callback functions with reduced dependencies + // Optimized callback functions with reduced dependencies and haptics throttling const handleToggleLibrary = useCallback(() => { - Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium); + if (isScreenFocused) { + Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium); + } toggleLibrary(); - }, [inLibrary, toggleLibrary]); + }, [inLibrary, toggleLibrary, isScreenFocused]); const handleSeasonChangeWithHaptics = useCallback((seasonNumber: number) => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (isScreenFocused) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } handleSeasonChange(seasonNumber); - }, [handleSeasonChange]); + }, [handleSeasonChange, isScreenFocused]); const handleShowStreams = useCallback(() => { const { watchProgress } = watchProgressData; @@ -302,26 +341,38 @@ const MetadataScreen: React.FC = () => { }, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]); const handleEpisodeSelect = useCallback((episode: Episode) => { - console.log('[MetadataScreen] Selected Episode:', JSON.stringify(episode, null, 2)); + if (!isScreenFocused) return; + + console.log('[MetadataScreen] Selected Episode:', episode.episode_number, episode.season_number); const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`; - navigation.navigate('Streams', { - id, - type, - episodeId, - episodeThumbnail: episode.still_path || undefined + + // Optimize navigation with requestAnimationFrame + requestAnimationFrame(() => { + navigation.navigate('Streams', { + id, + type, + episodeId, + episodeThumbnail: episode.still_path || undefined + }); }); - }, [navigation, id, type]); + }, [navigation, id, type, isScreenFocused]); - const handleBack = useCallback(() => navigation.goBack(), [navigation]); + const handleBack = useCallback(() => { + if (isScreenFocused) { + navigation.goBack(); + } + }, [navigation, isScreenFocused]); + const handleSelectCastMember = useCallback((castMember: any) => { + if (!isScreenFocused) return; setSelectedCastMember(castMember); setShowCastModal(true); - }, []); + }, [isScreenFocused]); - // Ultra-optimized animated styles - minimal calculations + // Ultra-optimized animated styles - minimal calculations with conditional updates const containerStyle = useAnimatedStyle(() => ({ - opacity: animations.screenOpacity.value, - }), []); + opacity: isScreenFocused ? animations.screenOpacity.value : 0.8, + }), [isScreenFocused]); const contentStyle = useAnimatedStyle(() => ({ opacity: animations.contentOpacity.value, @@ -441,21 +492,23 @@ const MetadataScreen: React.FC = () => { metadata={metadata} imdbId={imdbId} type={type as 'movie' | 'series'} - renderRatings={() => imdbId ? ( - + renderRatings={() => imdbId && shouldLoadSecondaryData ? ( + ) : null} /> - {/* Cast Section with skeleton when loading */} - + {/* Cast Section with skeleton when loading - Lazy loaded */} + {shouldLoadSecondaryData && ( + + )} - {/* Recommendations Section with skeleton when loading */} - {type === 'movie' && ( - @@ -463,7 +516,7 @@ const MetadataScreen: React.FC = () => { {/* Series/Movie Content with episode skeleton when loading */} {type === 'series' ? ( - { metadata={metadata || undefined} /> ) : ( - metadata && + metadata && )} )} - {/* Cast Details Modal */} - setShowCastModal(false)} - castMember={selectedCastMember} - /> + {/* Cast Details Modal - Memoized */} + {showCastModal && ( + setShowCastModal(false)} + castMember={selectedCastMember} + /> + )} ); }; @@ -577,4 +632,16 @@ const styles = StyleSheet.create({ }, }); +// Performance Optimizations Applied: +// 1. Memoized components (Cast, Series, Movie, Ratings, Modal) +// 2. Lazy loading of secondary data (cast, recommendations, ratings) +// 3. Focus-based rendering and interaction management +// 4. Debounced Trakt progress fetching with reduced logging +// 5. Optimized callback functions with screen focus checks +// 6. Conditional haptics feedback based on screen focus +// 7. Memory management and cleanup on unmount +// 8. Performance monitoring in development mode +// 9. Reduced re-renders through better state management +// 10. RequestAnimationFrame for navigation optimization + export default MetadataScreen; \ No newline at end of file