Skip to content

Commit ccaad57

Browse files
rubenfiszelclaude
andcommitted
feat: optimize landing page video loading and caching
- Compress landing videos (19MB → 5-6.6MB each, ~70% reduction) - Add Cloudflare _headers for aggressive video/image caching - Implement lazy loading: videos only load when tab is selected - Add loading indicator with progress percentage while buffering - Re-encode videos with cues at front for faster streaming Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 610cb1e commit ccaad57

File tree

5 files changed

+145
-12
lines changed

5 files changed

+145
-12
lines changed

src/landing/components/ProductionTabs.tsx

Lines changed: 134 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,19 @@ export default function ProductionTabs({
7878
const containerRef = useRef<HTMLDivElement>(null);
7979
const [hasBeenVisible, setHasBeenVisible] = useState(false);
8080

81+
// Lazy loading state - track which tabs have been visited
82+
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(new Set([tabs[0]?.id || 'scripts']));
83+
// Loading state per video
84+
const [loadingState, setLoadingState] = useState<Record<string, { loading: boolean; progress: number }>>({});
85+
8186
const getCurrentVideo = () => videoRefs.current[selectedTab];
8287

88+
// Mark tab as visited when selected
89+
const handleTabSelect = (tabId: string) => {
90+
setSelectedTab(tabId);
91+
setVisitedTabs(prev => new Set([...prev, tabId]));
92+
};
93+
8394
// Detect when component is visible in viewport
8495
useEffect(() => {
8596
const container = containerRef.current;
@@ -98,6 +109,78 @@ export default function ProductionTabs({
98109
return () => observer.disconnect();
99110
}, [hasBeenVisible]);
100111

112+
// Track video loading progress
113+
useEffect(() => {
114+
const video = videoRefs.current[selectedTab];
115+
if (!video || !visitedTabs.has(selectedTab)) return;
116+
117+
const handleLoadStart = () => {
118+
setLoadingState(prev => ({
119+
...prev,
120+
[selectedTab]: { loading: true, progress: 0 }
121+
}));
122+
};
123+
124+
const handleProgress = () => {
125+
if (video.buffered.length > 0 && video.duration > 0) {
126+
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
127+
const progressPercent = (bufferedEnd / video.duration) * 100;
128+
setLoadingState(prev => ({
129+
...prev,
130+
[selectedTab]: { loading: progressPercent < 100, progress: progressPercent }
131+
}));
132+
}
133+
};
134+
135+
const handleCanPlayThrough = () => {
136+
setLoadingState(prev => ({
137+
...prev,
138+
[selectedTab]: { loading: false, progress: 100 }
139+
}));
140+
};
141+
142+
const handleWaiting = () => {
143+
setLoadingState(prev => ({
144+
...prev,
145+
[selectedTab]: { ...prev[selectedTab], loading: true }
146+
}));
147+
};
148+
149+
const handlePlaying = () => {
150+
setLoadingState(prev => ({
151+
...prev,
152+
[selectedTab]: { ...prev[selectedTab], loading: false }
153+
}));
154+
};
155+
156+
video.addEventListener('loadstart', handleLoadStart);
157+
video.addEventListener('progress', handleProgress);
158+
video.addEventListener('canplaythrough', handleCanPlayThrough);
159+
video.addEventListener('waiting', handleWaiting);
160+
video.addEventListener('playing', handlePlaying);
161+
162+
// Check initial state
163+
if (video.readyState >= 4) {
164+
setLoadingState(prev => ({
165+
...prev,
166+
[selectedTab]: { loading: false, progress: 100 }
167+
}));
168+
} else if (video.readyState < 3) {
169+
setLoadingState(prev => ({
170+
...prev,
171+
[selectedTab]: { loading: true, progress: 0 }
172+
}));
173+
}
174+
175+
return () => {
176+
video.removeEventListener('loadstart', handleLoadStart);
177+
video.removeEventListener('progress', handleProgress);
178+
video.removeEventListener('canplaythrough', handleCanPlayThrough);
179+
video.removeEventListener('waiting', handleWaiting);
180+
video.removeEventListener('playing', handlePlaying);
181+
};
182+
}, [selectedTab, visitedTabs]);
183+
101184
// Play/pause videos when tab changes
102185
useEffect(() => {
103186
// Pause all videos first
@@ -253,7 +336,7 @@ export default function ProductionTabs({
253336
{tabs.map((tab) => (
254337
<button
255338
key={tab.id}
256-
onClick={() => setSelectedTab(tab.id)}
339+
onClick={() => handleTabSelect(tab.id)}
257340
className={`flex-1 px-4 py-2 font-medium text-sm transition-colors border-b-2 text-center text-gray-900 dark:text-white ${
258341
selectedTab === tab.id
259342
? 'border-blue-500'
@@ -276,18 +359,57 @@ export default function ProductionTabs({
276359
{tab.description}
277360
</p>
278361
{/* Video */}
279-
<div className="relative group/video rounded-lg overflow-hidden">
280-
<video
281-
ref={(el) => { videoRefs.current[tab.id] = el; }}
282-
className="w-full object-cover"
283-
loop
284-
muted
285-
playsInline
286-
>
287-
<source src={tab.video} type="video/webm" />
288-
</video>
362+
<div className="relative group/video rounded-lg overflow-hidden bg-gray-900">
363+
{visitedTabs.has(tab.id) ? (
364+
<video
365+
ref={(el) => { videoRefs.current[tab.id] = el; }}
366+
className="w-full object-cover"
367+
loop
368+
muted
369+
playsInline
370+
preload="auto"
371+
>
372+
<source src={tab.video} type="video/webm" />
373+
</video>
374+
) : (
375+
<div className="w-full aspect-video" />
376+
)}
377+
{/* Loading overlay with progress */}
378+
{selectedTab === tab.id && loadingState[tab.id]?.loading && (
379+
<div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-900/90 backdrop-blur-sm">
380+
<div className="w-16 h-16 mb-4 relative">
381+
<svg className="w-full h-full" viewBox="0 0 50 50">
382+
<circle
383+
cx="25"
384+
cy="25"
385+
r="20"
386+
fill="none"
387+
stroke="rgba(255, 255, 255, 0.2)"
388+
strokeWidth="4"
389+
/>
390+
<circle
391+
cx="25"
392+
cy="25"
393+
r="20"
394+
fill="none"
395+
stroke="rgba(59, 130, 246, 0.9)"
396+
strokeWidth="4"
397+
strokeDasharray={2 * Math.PI * 20}
398+
strokeDashoffset={2 * Math.PI * 20 * (1 - (loadingState[tab.id]?.progress || 0) / 100)}
399+
strokeLinecap="round"
400+
className="transition-all duration-300"
401+
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}
402+
/>
403+
</svg>
404+
<span className="absolute inset-0 flex items-center justify-center text-white text-xs font-medium">
405+
{Math.round(loadingState[tab.id]?.progress || 0)}%
406+
</span>
407+
</div>
408+
<p className="text-white/70 text-sm">Loading video...</p>
409+
</div>
410+
)}
289411
{/* Subtitle overlay */}
290-
{enableSubtitles && selectedTab === tab.id && currentSubtitle && (
412+
{enableSubtitles && selectedTab === tab.id && currentSubtitle && !loadingState[tab.id]?.loading && (
291413
<div
292414
className="absolute inset-0 flex items-center justify-center bg-gray-900/80 backdrop-blur-[2px] transition-opacity duration-300 cursor-pointer"
293415
onClick={skipSubtitle}

static/_headers

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Video files - immutable caching (1 year)
2+
/videos/*
3+
Cache-Control: public, max-age=31536000, immutable
4+
5+
# Images - long cache with revalidation
6+
/images/*
7+
Cache-Control: public, max-age=2592000, stale-while-revalidate=86400
8+
9+
# Fonts - immutable caching
10+
/fonts/*
11+
Cache-Control: public, max-age=31536000, immutable

static/videos/landingapps-ui.webm

-11.5 MB
Binary file not shown.

static/videos/landingflows-ui.webm

-13.3 MB
Binary file not shown.
-13.4 MB
Binary file not shown.

0 commit comments

Comments
 (0)