From 9a955fa25fcc006b87edb562b94ac06390663604 Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Fri, 26 Dec 2025 17:45:57 -0500 Subject: [PATCH] feat: Add lazy loading attribute to images --- frontend/src/components/CollectionCard.tsx | 1 + frontend/src/components/VideoCard.tsx | 28 ++++++++++++++++--- .../components/VideoPlayer/UpNextSidebar.tsx | 1 + .../src/components/VideoPlayer/VideoInfo.tsx | 8 ++---- frontend/src/contexts/VideoContext.tsx | 19 +++++++++---- frontend/src/hooks/useVideoResolution.ts | 19 ++++++++++--- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/CollectionCard.tsx b/frontend/src/components/CollectionCard.tsx index 2e00204..30a3050 100644 --- a/frontend/src/components/CollectionCard.tsx +++ b/frontend/src/components/CollectionCard.tsx @@ -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%', diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx index cc5e5cd..32607ee 100644 --- a/frontend/src/components/VideoCard.tsx +++ b/frontend/src/components/VideoCard.tsx @@ -55,6 +55,7 @@ const VideoCard: React.FC = ({ const [isVideoPlaying, setIsVideoPlaying] = useState(false); const [isImageLoaded, setIsImageLoaded] = useState(false); const videoRef = useRef(null); + const hoverTimeoutRef = useRef(null); // New state for player menu const [playerMenuAnchor, setPlayerMenuAnchor] = useState(null); @@ -64,32 +65,50 @@ const VideoCard: React.FC = ({ 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 = ({ component="img" image={thumbnailSrc || 'https://via.placeholder.com/480x360?text=No+Thumbnail'} alt={`${video.title} thumbnail`} + loading="lazy" onLoad={() => setIsImageLoaded(true)} sx={{ position: 'absolute', diff --git a/frontend/src/components/VideoPlayer/UpNextSidebar.tsx b/frontend/src/components/VideoPlayer/UpNextSidebar.tsx index 125b83d..c3504de 100644 --- a/frontend/src/components/VideoPlayer/UpNextSidebar.tsx +++ b/frontend/src/components/VideoPlayer/UpNextSidebar.tsx @@ -60,6 +60,7 @@ const SidebarThumbnail: React.FC<{ video: Video }> = ({ video }) => { )} Promise; @@ -50,7 +48,7 @@ const VideoInfo: React.FC = ({ 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 = ({ return ( - {/* Hidden video element to get resolution */} - {(videoUrl || video.sourceUrl) && ( + {/* Hidden video element to get resolution - only create if needed */} + {needsDetection && (videoUrl || video.sourceUrl) && (