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' }}
style={styles.poster}
contentFit="cover"
cachePolicy="memory-disk"
transition={300}
cachePolicy="memory"
transition={200}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
/>
<LinearGradient
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}>
<ExpoImage
source={{ uri: item.poster }}
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
contentFit="cover"
transition={300}
cachePolicy="memory-disk"
recyclingKey={`poster-${item.id}`}
cachePolicy="memory"
transition={200}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
onLoadStart={() => {
setImageLoaded(false);
setImageError(false);

View file

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

View file

@ -99,16 +99,35 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
// Preload the image
const preloadImage = async (url: string): Promise<boolean> => {
if (!url) return false;
// Skip if already cached to prevent redundant prefetch
if (imageCache[url]) return true;
try {
// For Metahub logos, only do validation if enabled
// Note: Temporarily disable metahub validation until fixed
if (false && url.includes('metahub.space')) {
// Basic URL validation
if (!url || typeof url !== 'string') return false;
// 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 {
const isValid = await isValidMetahubLogo(url);
if (!isValid) {
// For URLs without clear image extensions, do a quick HEAD request
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;
}
} catch (validationError) {
@ -117,10 +136,22 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
}
// 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;
return true;
} catch (error) {
// Clear any partial cache entry on error
delete imageCache[url];
return false;
}
};
@ -423,8 +454,9 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory-disk"
transition={400}
cachePolicy="memory"
transition={300}
recyclingKey={`logo-${featuredContent.id}`}
onError={onLogoLoadError}
/>
</Animated.View>

View file

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

View file

@ -325,21 +325,40 @@ const HomeScreen = () => {
if (!content.length) return;
try {
const imagePromises = content.map(item => {
const imagesToLoad = [
item.poster,
item.banner,
item.logo
].filter(Boolean) as string[];
return Promise.all(
imagesToLoad.map(imageUrl =>
ExpoImage.prefetch(imageUrl)
)
);
});
await Promise.all(imagePromises);
// Limit concurrent prefetching to prevent memory pressure
const MAX_CONCURRENT_PREFETCH = 5;
const BATCH_SIZE = 3;
const allImages = content.slice(0, 10) // Limit total images to prefetch
.map(item => [item.poster, item.banner, item.logo])
.flat()
.filter(Boolean) as string[];
// 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(
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) {
// Silently handle preload errors
}
@ -353,11 +372,27 @@ const HomeScreen = () => {
if (!featuredContent) return;
try {
// Lock orientation to landscape before navigation to prevent glitches
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
// Clear image cache to reduce memory pressure before orientation change
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
await new Promise(resolve => setTimeout(resolve, 100));
// Lock orientation to landscape before navigation to prevent glitches
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', {
uri: stream.url,
@ -369,6 +404,8 @@ const HomeScreen = () => {
type: featuredContent.type
});
} catch (error) {
logger.error('[HomeScreen] Error in handlePlayStream:', error);
// Fallback: navigate anyway
navigation.navigate('Player', {
uri: stream.url,