perfomance optimziations

This commit is contained in:
tapframe 2025-08-06 21:41:23 +05:30
parent 62a2ed0046
commit cc494bdf17
2 changed files with 342 additions and 187 deletions

View file

@ -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<HeroSectionProps> = ({
/**
* 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<HeroSectionProps> = memo(({
metadata,
bannerImage,
loadingBanner,
@ -650,21 +680,47 @@ const HeroSection: React.FC<HeroSectionProps> = ({
}) => {
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<HeroSectionProps> = ({
// 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<HeroSectionProps> = ({
}
}, [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<HeroSectionProps> = ({
}]
}), []);
// 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<HeroSectionProps> = ({
entering={FadeIn.duration(400).delay(200 + index * 100)}
style={{ flexDirection: 'row', alignItems: 'center' }}
>
<Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
<Text style={[styles.genreText, { color: themeColors.text }]}>
{genreName}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot, { color: currentTheme.colors.text }]}></Text>
<Text style={[styles.genreDot, { color: themeColors.text }]}></Text>
)}
</Animated.View>
));
}, [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<HeroSectionProps> = ({
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 (
<Animated.View style={[styles.heroSection, heroAnimatedStyle]}>
{/* Optimized Background */}
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
<View style={[styles.absoluteFill, { backgroundColor: themeColors.black }]} />
{/* Loading placeholder for smooth transition */}
{((imageSource && !imageLoaded) || loadingBanner) && (
{/* Optimized shimmer loading effect */}
{shouldLoadSecondaryData && ((imageSource && !imageLoaded) || loadingBanner) && (
<Animated.View style={[styles.absoluteFill, {
opacity: shimmerOpacity,
}]}>
@ -830,8 +918,8 @@ const HeroSection: React.FC<HeroSectionProps> = ({
</Animated.View>
)}
{/* Enhanced Background Image with smooth loading */}
{imageSource && !loadingBanner && (
{/* Optimized background image with lazy loading */}
{shouldLoadSecondaryData && imageSource && !loadingBanner && (
<Animated.Image
source={{ uri: imageSource }}
style={[styles.absoluteFill, backdropImageStyle]}
@ -841,13 +929,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({
/>
)}
{/* Simplified Gradient */}
{/* Optimized Gradient */}
<LinearGradient
colors={[
'rgba(0,0,0,0)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground
themeColors.darkBackground
]}
locations={[0, 0.6, 0.85, 1]}
style={styles.heroGradient}
@ -856,7 +944,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
{/* Optimized Title/Logo */}
<View style={styles.logoContainer}>
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
{metadata.logo && !logoLoadError ? (
{shouldLoadSecondaryData && metadata.logo && !logoLoadError ? (
<Image
source={{ uri: metadata.logo }}
style={styles.titleLogo}
@ -867,7 +955,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
}}
/>
) : (
<Text style={[styles.heroTitle, { color: currentTheme.colors.highEmphasis }]}>
<Text style={[styles.heroTitle, { color: themeColors.highEmphasis }]}>
{metadata.name}
</Text>
)}
@ -883,8 +971,8 @@ const HeroSection: React.FC<HeroSectionProps> = ({
isWatched={isWatched}
/>
{/* Optimized Genres */}
{genreElements && (
{/* Optimized genre display with lazy loading */}
{shouldLoadSecondaryData && genreElements && (
<View style={styles.genreContainer}>
{genreElements}
</View>
@ -908,7 +996,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
</LinearGradient>
</Animated.View>
);
};
});
// Ultra-optimized styles
const styles = StyleSheet.create({
@ -1305,4 +1393,4 @@ const styles = StyleSheet.create({
},
});
export default React.memo(HeroSection);
export default HeroSection;

View file

@ -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<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -59,7 +69,10 @@ const MetadataScreen: React.FC = () => {
const [isContentReady, setIsContentReady] = useState(false);
const [showCastModal, setShowCastModal] = useState(false);
const [selectedCastMember, setSelectedCastMember] = useState<any>(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 ? (
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
renderRatings={() => imdbId && shouldLoadSecondaryData ? (
<MemoizedRatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
) : null}
/>
{/* Cast Section with skeleton when loading */}
<CastSection
cast={cast}
loadingCast={loadingCast}
onSelectCastMember={handleSelectCastMember}
/>
{/* Cast Section with skeleton when loading - Lazy loaded */}
{shouldLoadSecondaryData && (
<MemoizedCastSection
cast={cast}
loadingCast={loadingCast}
onSelectCastMember={handleSelectCastMember}
/>
)}
{/* Recommendations Section with skeleton when loading */}
{type === 'movie' && (
<MoreLikeThisSection
{/* Recommendations Section with skeleton when loading - Lazy loaded */}
{type === 'movie' && shouldLoadSecondaryData && (
<MemoizedMoreLikeThisSection
recommendations={recommendations}
loadingRecommendations={loadingRecommendations}
/>
@ -463,7 +516,7 @@ const MetadataScreen: React.FC = () => {
{/* Series/Movie Content with episode skeleton when loading */}
{type === 'series' ? (
<SeriesContent
<MemoizedSeriesContent
episodes={Object.values(groupedEpisodes).flat()}
selectedSeason={selectedSeason}
loadingSeasons={loadingSeasons}
@ -473,19 +526,21 @@ const MetadataScreen: React.FC = () => {
metadata={metadata || undefined}
/>
) : (
metadata && <MovieContent metadata={metadata} />
metadata && <MemoizedMovieContent metadata={metadata} />
)}
</Animated.View>
</Animated.ScrollView>
</>
)}
{/* Cast Details Modal */}
<CastDetailsModal
visible={showCastModal}
onClose={() => setShowCastModal(false)}
castMember={selectedCastMember}
/>
{/* Cast Details Modal - Memoized */}
{showCastModal && (
<MemoizedCastDetailsModal
visible={showCastModal}
onClose={() => setShowCastModal(false)}
castMember={selectedCastMember}
/>
)}
</SafeAreaView>
);
};
@ -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;