feat: Add lazy loading attribute to images
This commit is contained in:
@@ -125,6 +125,7 @@ const CollectionThumbnail: React.FC<{ video: Video; index: number }> = ({ video,
|
||||
component="img"
|
||||
image={src}
|
||||
alt={video.title}
|
||||
loading="lazy"
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
@@ -55,6 +55,7 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
||||
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// New state for player menu
|
||||
const [playerMenuAnchor, setPlayerMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
@@ -64,32 +65,50 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
const { showSnackbar } = useSnackbar();
|
||||
const { updateVideo, incrementView } = useVideo();
|
||||
|
||||
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isMobile && video.videoPath) {
|
||||
setIsHovered(true);
|
||||
// Add delay before loading video to prevent loading on quick hovers
|
||||
// This reduces memory usage when quickly moving mouse over multiple cards
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
setIsHovered(true);
|
||||
}, 300); // 300ms delay
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Clear hover timeout if mouse leaves before delay
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setIsHovered(false);
|
||||
setIsVideoPlaying(false);
|
||||
// Cleanup video element when mouse leaves
|
||||
|
||||
// Aggressively cleanup video element when mouse leaves
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
videoRef.current.src = '';
|
||||
videoRef.current.load();
|
||||
// Force garbage collection hint
|
||||
videoRef.current.removeAttribute('src');
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup video element on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clear any pending hover timeout
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Aggressively cleanup video element
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
videoRef.current.src = '';
|
||||
videoRef.current.load();
|
||||
videoRef.current.removeAttribute('src');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -443,6 +462,7 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
component="img"
|
||||
image={thumbnailSrc || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
||||
alt={`${video.title} thumbnail`}
|
||||
loading="lazy"
|
||||
onLoad={() => setIsImageLoaded(true)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -60,6 +60,7 @@ const SidebarThumbnail: React.FC<{ video: Video }> = ({ video }) => {
|
||||
)}
|
||||
<CardMedia
|
||||
component="img"
|
||||
loading="lazy"
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 94,
|
||||
|
||||
@@ -11,8 +11,6 @@ import VideoMetadata from './VideoInfo/VideoMetadata';
|
||||
import VideoRating from './VideoInfo/VideoRating';
|
||||
import VideoTags from './VideoInfo/VideoTags';
|
||||
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
interface VideoInfoProps {
|
||||
video: Video;
|
||||
onTitleSave: (newTitle: string) => Promise<void>;
|
||||
@@ -50,7 +48,7 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
|
||||
onUnsubscribe,
|
||||
onToggleVisibility
|
||||
}) => {
|
||||
const { videoRef, videoResolution } = useVideoResolution(video);
|
||||
const { videoRef, videoResolution, needsDetection } = useVideoResolution(video);
|
||||
const videoUrl = useCloudStorageUrl(video.videoPath, 'video');
|
||||
|
||||
// Cleanup video element on unmount
|
||||
@@ -67,8 +65,8 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{/* Hidden video element to get resolution */}
|
||||
{(videoUrl || video.sourceUrl) && (
|
||||
{/* Hidden video element to get resolution - only create if needed */}
|
||||
{needsDetection && (videoUrl || video.sourceUrl) && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl || video.sourceUrl}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useSnackbar } from './SnackbarContext';
|
||||
import { useVisitorMode } from './VisitorModeContext';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const MAX_SEARCH_RESULTS = 200; // Maximum number of search results to keep in memory
|
||||
|
||||
interface VideoContextType {
|
||||
videos: Video[];
|
||||
@@ -69,7 +70,7 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
retry: 3, // Reduced from 10 to 3
|
||||
retryDelay: 1000,
|
||||
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
|
||||
gcTime: 30 * 60 * 1000, // Garbage collect after 30 minutes (videos are important data)
|
||||
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes (reduced from 30 to save memory)
|
||||
});
|
||||
|
||||
// Filter invisible videos when in visitor mode
|
||||
@@ -90,7 +91,7 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
retry: 3, // Reduced from 10 to 3
|
||||
retryDelay: 1000,
|
||||
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
|
||||
gcTime: 15 * 60 * 1000, // Garbage collect after 15 minutes
|
||||
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes (reduced from 15 to save memory)
|
||||
});
|
||||
|
||||
const availableTags = settingsData?.tags || [];
|
||||
@@ -240,7 +241,9 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
});
|
||||
|
||||
if (!signal.aborted) {
|
||||
setSearchResults(response.data.results);
|
||||
// Limit search results to prevent memory issues
|
||||
const results = response.data.results || [];
|
||||
setSearchResults(results.slice(0, MAX_SEARCH_RESULTS));
|
||||
}
|
||||
} catch (youtubeErr: any) {
|
||||
if (youtubeErr.name !== 'CanceledError' && youtubeErr.name !== 'AbortError') {
|
||||
@@ -278,6 +281,11 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
// Use ref check first to prevent race conditions (immediate, synchronous check)
|
||||
if (!searchTerm || loadMoreInProgress.current || loadingMore || !showYoutubeSearch) return;
|
||||
|
||||
// Don't load more if we've reached the maximum
|
||||
if (searchResults.length >= MAX_SEARCH_RESULTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Set both state and ref to prevent concurrent requests
|
||||
loadMoreInProgress.current = true;
|
||||
@@ -301,8 +309,9 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
const existingIds = new Set(prev.map(result => result.id));
|
||||
// Filter out duplicates by ID
|
||||
const newResults = response.data.results.filter((result: any) => !existingIds.has(result.id));
|
||||
// Only append new, non-duplicate results
|
||||
return [...prev, ...newResults];
|
||||
// Only append new, non-duplicate results, up to MAX_SEARCH_RESULTS
|
||||
const combined = [...prev, ...newResults];
|
||||
return combined.slice(0, MAX_SEARCH_RESULTS);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -54,7 +54,19 @@ export const useVideoResolution = (video: Video) => {
|
||||
);
|
||||
const videoUrl = useCloudStorageUrl(video.videoPath, "video");
|
||||
|
||||
// First check if resolution is already available in video object
|
||||
const resolutionFromObject = formatResolution(video);
|
||||
|
||||
// Only create video element if resolution is not available in video object
|
||||
const needsDetection = !resolutionFromObject;
|
||||
|
||||
useEffect(() => {
|
||||
// Skip video element creation if resolution is already available
|
||||
if (!needsDetection) {
|
||||
setDetectedResolution(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const videoElement = videoRef.current;
|
||||
const videoSrc = videoUrl || video.sourceUrl;
|
||||
if (!videoElement || !videoSrc) {
|
||||
@@ -105,11 +117,10 @@ export const useVideoResolution = (video: Video) => {
|
||||
videoElement.src = "";
|
||||
videoElement.load();
|
||||
};
|
||||
}, [videoUrl, video.sourceUrl, video.id]);
|
||||
}, [needsDetection, videoUrl, video.sourceUrl, video.id]);
|
||||
|
||||
// Try to get resolution from video object first, fallback to detected resolution
|
||||
const resolutionFromObject = formatResolution(video);
|
||||
// Use resolution from object if available, otherwise use detected resolution
|
||||
const videoResolution = resolutionFromObject || detectedResolution;
|
||||
|
||||
return { videoRef, videoResolution };
|
||||
return { videoRef, videoResolution, needsDetection };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user