Skip to content

Commit c3eeb92

Browse files
wimpywarlordclaude
andcommitted
Implement lazy loading for images and videos to improve page performance
- Add Intersection Observer API for video lazy loading - Videos load 200px before entering viewport for smooth experience - Add loading placeholder with pulse animation for videos - Explicit lazy loading attribute for images - Prevents downloading off-screen media on initial page load - Significant performance improvement for long blog posts with many assets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 4364399 commit c3eeb92

File tree

2 files changed

+72
-19
lines changed

2 files changed

+72
-19
lines changed

src/components/gallery/GalleryImage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const GalleryImage = ({ enableGallery = true, ...props }: GalleryImagePro
3232
ref={imageRef}
3333
className={`${props.className} ${enableGallery ? "cursor-pointer" : ""}`}
3434
onClick={enableGallery ? handleClick : props.onClick}
35+
loading={props.loading || "lazy"} // Explicit lazy loading (images load as user scrolls)
3536
/>
3637
);
3738
};

src/components/gallery/GalleryVideo.tsx

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useRef, VideoHTMLAttributes, useState, useEffect } from "react";
3+
import { useRef, VideoHTMLAttributes, useState, useEffect, ReactElement } from "react";
44
import { useGallery } from "./GalleryViewer";
55

66
interface GalleryVideoProps extends VideoHTMLAttributes<HTMLVideoElement> {
@@ -13,17 +13,56 @@ export const GalleryVideo = ({ enableGallery = true, children, ...props }: Galle
1313
const expandedVideoRef = useRef<HTMLVideoElement>(null);
1414
const { openGallery } = useGallery();
1515
const [videoSrc, setVideoSrc] = useState<string | undefined>(undefined);
16+
const [isLoaded, setIsLoaded] = useState(false);
1617

18+
// Extract video source from children on mount
1719
useEffect(() => {
18-
// Extract video source from children
19-
if (videoRef.current) {
20-
const sourceElement = videoRef.current.querySelector("source");
21-
if (sourceElement) {
22-
setVideoSrc(sourceElement.src);
20+
const extractSrc = () => {
21+
const childArray = Array.isArray(children) ? children : [children];
22+
const sourceChild = childArray.find(
23+
(child: any) => child?.type === "source"
24+
) as ReactElement<{ src: string }>;
25+
26+
if (sourceChild?.props?.src) {
27+
setVideoSrc(sourceChild.props.src);
2328
}
24-
}
29+
};
30+
31+
extractSrc();
2532
}, [children]);
2633

34+
// Lazy loading with Intersection Observer
35+
useEffect(() => {
36+
const videoElement = videoRef.current;
37+
if (!videoElement || !videoSrc) return;
38+
39+
const observer = new IntersectionObserver(
40+
(entries) => {
41+
entries.forEach((entry) => {
42+
if (entry.isIntersecting && !isLoaded) {
43+
// Load video when it enters viewport
44+
const sourceElement = videoElement.querySelector("source");
45+
if (sourceElement && videoSrc) {
46+
sourceElement.src = videoSrc;
47+
videoElement.load();
48+
setIsLoaded(true);
49+
}
50+
}
51+
});
52+
},
53+
{
54+
rootMargin: "200px", // Start loading 200px before entering viewport
55+
threshold: 0.01,
56+
}
57+
);
58+
59+
observer.observe(videoElement);
60+
61+
return () => {
62+
observer.disconnect();
63+
};
64+
}, [isLoaded, videoSrc]);
65+
2766
const handleClick = () => {
2867
if (enableGallery && videoRef.current) {
2968
// Pause the thumbnail video
@@ -51,17 +90,30 @@ export const GalleryVideo = ({ enableGallery = true, children, ...props }: Galle
5190
};
5291

5392
return (
54-
<video
55-
{...props}
56-
ref={videoRef}
57-
className={`${props.className} ${enableGallery ? "cursor-pointer" : ""}`}
58-
onClick={enableGallery ? handleClick : props.onClick}
59-
autoPlay
60-
loop
61-
muted
62-
playsInline
63-
>
64-
{children}
65-
</video>
93+
<div className="relative">
94+
{/* Loading placeholder */}
95+
{!isLoaded && (
96+
<div
97+
className="absolute inset-0 bg-muted/50 animate-pulse rounded-lg flex items-center justify-center"
98+
style={{ aspectRatio: "16/9" }}
99+
>
100+
<div className="text-muted-foreground text-sm">Loading...</div>
101+
</div>
102+
)}
103+
104+
<video
105+
{...props}
106+
ref={videoRef}
107+
className={`${props.className} ${enableGallery ? "cursor-pointer" : ""} ${!isLoaded ? "opacity-0" : "opacity-100"} transition-opacity duration-300`}
108+
onClick={enableGallery ? handleClick : props.onClick}
109+
autoPlay={isLoaded}
110+
loop
111+
muted
112+
playsInline
113+
>
114+
{/* Render source without src initially - will be set by Intersection Observer */}
115+
<source type="video/mp4" />
116+
</video>
117+
</div>
66118
);
67119
};

0 commit comments

Comments
 (0)