Refactor image handling and prefetching logic across multiple components for improved performance and memory management

This update modifies the image handling in ContentItem, ContinueWatchingSection, and FeaturedContent components to utilize a placeholder image and adjust caching strategies. Additionally, the HomeScreen component has been enhanced to limit concurrent image prefetching, reducing memory pressure and improving overall performance. These changes aim to create a smoother user experience while navigating through content.
This commit is contained in:
tapframe 2025-06-21 18:47:46 +05:30
parent 4a6f349cdb
commit 9ac99ee0a3
6 changed files with 157 additions and 80 deletions

View file

@ -27,8 +27,11 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }} source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster} style={styles.poster}
contentFit="cover" contentFit="cover"
cachePolicy="memory-disk" cachePolicy="memory"
transition={300} transition={200}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
/> />
<LinearGradient <LinearGradient
colors={['transparent', 'rgba(0,0,0,0.85)']} colors={['transparent', 'rgba(0,0,0,0.85)']}

View file

@ -99,12 +99,14 @@ const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
> >
<View style={styles.contentItemContainer}> <View style={styles.contentItemContainer}>
<ExpoImage <ExpoImage
source={{ uri: item.poster }} source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster} style={styles.poster}
contentFit="cover" contentFit="cover"
transition={300} cachePolicy="memory"
cachePolicy="memory-disk" transition={200}
recyclingKey={`poster-${item.id}`} placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
onLoadStart={() => { onLoadStart={() => {
setImageLoaded(false); setImageLoaded(false);
setImageError(false); setImageError(false);

View file

@ -295,11 +295,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
{/* Poster Image */} {/* Poster Image */}
<View style={styles.posterContainer}> <View style={styles.posterContainer}>
<ExpoImage <ExpoImage
source={{ uri: item.poster }} source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.widePoster} style={styles.continueWatchingPoster}
contentFit="cover" contentFit="cover"
cachePolicy="memory"
transition={200} transition={200}
cachePolicy="memory-disk" placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
/> />
</View> </View>
@ -429,7 +432,7 @@ const styles = StyleSheet.create({
width: 80, width: 80,
height: '100%', height: '100%',
}, },
widePoster: { continueWatchingPoster: {
width: '100%', width: '100%',
height: '100%', height: '100%',
borderTopLeftRadius: 12, borderTopLeftRadius: 12,

View file

@ -99,16 +99,35 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
// Preload the image // Preload the image
const preloadImage = async (url: string): Promise<boolean> => { const preloadImage = async (url: string): Promise<boolean> => {
if (!url) return false; // Skip if already cached to prevent redundant prefetch
if (imageCache[url]) return true; if (imageCache[url]) return true;
try { try {
// For Metahub logos, only do validation if enabled // Basic URL validation
// Note: Temporarily disable metahub validation until fixed if (!url || typeof url !== 'string') return false;
if (false && url.includes('metahub.space')) {
// Check if URL appears to be a valid image URL
const urlLower = url.toLowerCase();
const hasImageExtension = /\.(jpg|jpeg|png|webp|svg)(\?.*)?$/i.test(url);
const isImageService = urlLower.includes('image') || urlLower.includes('poster') || urlLower.includes('banner') || urlLower.includes('logo');
if (!hasImageExtension && !isImageService) {
try { try {
const isValid = await isValidMetahubLogo(url); // For URLs without clear image extensions, do a quick HEAD request
if (!isValid) { const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch(url, {
method: 'HEAD',
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) return false;
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.startsWith('image/')) {
return false; return false;
} }
} catch (validationError) { } catch (validationError) {
@ -117,10 +136,22 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
} }
// Always attempt to prefetch the image regardless of format validation // Always attempt to prefetch the image regardless of format validation
await ExpoImage.prefetch(url); // Add timeout and retry logic for prefetch
const prefetchWithTimeout = () => {
return Promise.race([
ExpoImage.prefetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Prefetch timeout')), 5000)
)
]);
};
await prefetchWithTimeout();
imageCache[url] = true; imageCache[url] = true;
return true; return true;
} catch (error) { } catch (error) {
// Clear any partial cache entry on error
delete imageCache[url];
return false; return false;
} }
}; };
@ -423,8 +454,9 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
source={{ uri: logoUrl }} source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle} style={styles.featuredLogo as ImageStyle}
contentFit="contain" contentFit="contain"
cachePolicy="memory-disk" cachePolicy="memory"
transition={400} transition={300}
recyclingKey={`logo-${featuredContent.id}`}
onError={onLogoLoadError} onError={onLogoLoadError}
/> />
</Animated.View> </Animated.View>

View file

@ -63,30 +63,30 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
'worklet'; 'worklet';
try { try {
// Start with slightly reduced values and animate to full visibility // Start with slightly reduced values and animate to full visibility
screenOpacity.value = withTiming(1, { screenOpacity.value = withTiming(1, {
duration: 250, duration: 250,
easing: easings.fast easing: easings.fast
}); });
heroOpacity.value = withTiming(1, { heroOpacity.value = withTiming(1, {
duration: 300, duration: 300,
easing: easings.fast easing: easings.fast
}); });
heroScale.value = withSpring(1, ultraFastSpring); heroScale.value = withSpring(1, ultraFastSpring);
uiElementsOpacity.value = withTiming(1, { uiElementsOpacity.value = withTiming(1, {
duration: 400, duration: 400,
easing: easings.natural easing: easings.natural
}); });
uiElementsTranslateY.value = withSpring(0, fastSpring); uiElementsTranslateY.value = withSpring(0, fastSpring);
contentOpacity.value = withTiming(1, { contentOpacity.value = withTiming(1, {
duration: 350, duration: 350,
easing: easings.fast easing: easings.fast
}); });
} catch (error) { } catch (error) {
// Silently handle any animation errors // Silently handle any animation errors
console.warn('Animation error in enterAnimations:', error); console.warn('Animation error in enterAnimations:', error);
@ -95,7 +95,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
// Use runOnUI for better performance with error handling // Use runOnUI for better performance with error handling
try { try {
runOnUI(enterAnimations)(); runOnUI(enterAnimations)();
} catch (error) { } catch (error) {
console.warn('Failed to run enter animations:', error); console.warn('Failed to run enter animations:', error);
} }
@ -109,17 +109,17 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
'worklet'; 'worklet';
try { try {
progressOpacity.value = withTiming(hasProgress ? 1 : 0, { progressOpacity.value = withTiming(hasProgress ? 1 : 0, {
duration: hasProgress ? 200 : 150, duration: hasProgress ? 200 : 150,
easing: easings.fast easing: easings.fast
}); });
} catch (error) { } catch (error) {
console.warn('Animation error in updateProgress:', error); console.warn('Animation error in updateProgress:', error);
} }
}; };
try { try {
runOnUI(updateProgress)(); runOnUI(updateProgress)();
} catch (error) { } catch (error) {
console.warn('Failed to run progress animation:', error); console.warn('Failed to run progress animation:', error);
} }
@ -151,19 +151,19 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
'worklet'; 'worklet';
try { try {
const rawScrollY = event.contentOffset.y; const rawScrollY = event.contentOffset.y;
scrollY.value = rawScrollY; scrollY.value = rawScrollY;
// Single calculation for header threshold // Single calculation for header threshold
const threshold = height * 0.4 - safeAreaTop; const threshold = height * 0.4 - safeAreaTop;
const progress = rawScrollY > threshold ? 1 : 0; const progress = rawScrollY > threshold ? 1 : 0;
// Use single progress value for all header animations // Use single progress value for all header animations
if (headerProgress.value !== progress) { if (headerProgress.value !== progress) {
headerProgress.value = withTiming(progress, { headerProgress.value = withTiming(progress, {
duration: progress ? 200 : 150, duration: progress ? 200 : 150,
easing: easings.ultraFast easing: easings.ultraFast
}); });
} }
} catch (error) { } catch (error) {
console.warn('Animation error in scroll handler:', error); console.warn('Animation error in scroll handler:', error);

View file

@ -325,21 +325,40 @@ const HomeScreen = () => {
if (!content.length) return; if (!content.length) return;
try { try {
const imagePromises = content.map(item => { // Limit concurrent prefetching to prevent memory pressure
const imagesToLoad = [ const MAX_CONCURRENT_PREFETCH = 5;
item.poster, const BATCH_SIZE = 3;
item.banner,
item.logo const allImages = content.slice(0, 10) // Limit total images to prefetch
].filter(Boolean) as string[]; .map(item => [item.poster, item.banner, item.logo])
.flat()
return Promise.all( .filter(Boolean) as string[];
imagesToLoad.map(imageUrl =>
ExpoImage.prefetch(imageUrl) // Process in small batches to prevent memory pressure
) for (let i = 0; i < allImages.length; i += BATCH_SIZE) {
); const batch = allImages.slice(i, i + BATCH_SIZE);
});
try {
await Promise.all(imagePromises); await Promise.all(
batch.map(async (imageUrl) => {
try {
await ExpoImage.prefetch(imageUrl);
// Small delay between prefetches to reduce memory pressure
await new Promise(resolve => setTimeout(resolve, 10));
} catch (error) {
// Silently handle individual prefetch errors
}
})
);
// Delay between batches to allow GC
if (i + BATCH_SIZE < allImages.length) {
await new Promise(resolve => setTimeout(resolve, 50));
}
} catch (error) {
// Continue with next batch if current batch fails
}
}
} catch (error) { } catch (error) {
// Silently handle preload errors // Silently handle preload errors
} }
@ -353,11 +372,27 @@ const HomeScreen = () => {
if (!featuredContent) return; if (!featuredContent) return;
try { try {
// Lock orientation to landscape before navigation to prevent glitches // Clear image cache to reduce memory pressure before orientation change
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); if (typeof (global as any)?.ExpoImage?.clearMemoryCache === 'function') {
try {
(global as any).ExpoImage.clearMemoryCache();
} catch (e) {
// Ignore cache clear errors
}
}
// Small delay to ensure orientation is set before navigation // Lock orientation to landscape before navigation to prevent glitches
await new Promise(resolve => setTimeout(resolve, 100)); try {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
// Longer delay to ensure orientation is fully set before navigation
await new Promise(resolve => setTimeout(resolve, 200));
} catch (orientationError) {
// If orientation lock fails, continue anyway but log it
logger.warn('[HomeScreen] Orientation lock failed:', orientationError);
// Still add a small delay
await new Promise(resolve => setTimeout(resolve, 100));
}
navigation.navigate('Player', { navigation.navigate('Player', {
uri: stream.url, uri: stream.url,
@ -369,6 +404,8 @@ const HomeScreen = () => {
type: featuredContent.type type: featuredContent.type
}); });
} catch (error) { } catch (error) {
logger.error('[HomeScreen] Error in handlePlayStream:', error);
// Fallback: navigate anyway // Fallback: navigate anyway
navigation.navigate('Player', { navigation.navigate('Player', {
uri: stream.url, uri: stream.url,