Video & Media Optimization
Optimize video, audio, and other media for performance
TL;DR
Self-hosting video is expensive. Use Cloudinary or a dedicated video CDN for transcoding, adaptive streaming, and bandwidth savings. For cost optimization strategies, see the Cost Optimization Guide.
Videos and media files can dramatically impact page performance. This guide covers strategies for delivering media efficiently without sacrificing quality.
Video Optimization
Use Modern Formats
// Serve modern formats with fallbacks
<video controls preload="metadata">
<source src="/video.webm" type="video/webm" />
<source src="/video.mp4" type="video/mp4" />
</video>| Format | Browser Support | Size vs H.264 |
|---|---|---|
| AV1 | Chrome, Firefox | 30-50% smaller |
| VP9/WebM | Chrome, Firefox, Edge | 20-35% smaller |
| H.264/MP4 | All browsers | Baseline |
| HEVC/H.265 | Safari, some others | 25-50% smaller |
Lazy Loading Videos
'use client';
import { useRef, useEffect, useState } from 'react';
function LazyVideo({ src, poster }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (videoRef.current) {
observer.observe(videoRef.current);
}
return () => observer.disconnect();
}, []);
return (
<video
ref={videoRef}
poster={poster}
controls
preload="none"
>
{isVisible && <source src={src} type="video/mp4" />}
</video>
);
}Preload Strategies
// Don't preload - saves bandwidth
<video preload="none" poster="/thumbnail.jpg">
// Preload metadata only - small download
<video preload="metadata">
// Preload video - use sparingly
<video preload="auto">Poster Images
Always provide a poster to prevent CLS:
<video
poster="/video-poster.jpg"
style={{ aspectRatio: '16/9' }}
controls
>
<source src="/video.mp4" type="video/mp4" />
</video>Background Videos
Performance-Conscious Background Video
'use client';
import { useEffect, useRef, useState } from 'react';
function BackgroundVideo() {
const videoRef = useRef<HTMLVideoElement>(null);
const [canAutoplay, setCanAutoplay] = useState(false);
useEffect(() => {
// Check if user prefers reduced motion
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
// Check connection quality
const connection = (navigator as any).connection;
const isSlow = connection?.saveData ||
connection?.effectiveType === 'slow-2g' ||
connection?.effectiveType === '2g';
if (!prefersReducedMotion && !isSlow) {
setCanAutoplay(true);
}
}, []);
return (
<div className="relative h-screen">
{canAutoplay ? (
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
className="absolute inset-0 w-full h-full object-cover"
>
<source src="/hero-bg.webm" type="video/webm" />
<source src="/hero-bg.mp4" type="video/mp4" />
</video>
) : (
<img
src="/hero-bg-poster.jpg"
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div className="relative z-10">
{/* Content */}
</div>
</div>
);
}Video Streaming
HLS/DASH Streaming
'use client';
import { useEffect, useRef } from 'react';
import Hls from 'hls.js';
function StreamingVideo({ src }) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(src);
hls.attachMedia(video);
return () => hls.destroy();
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS support
video.src = src;
}
}, [src]);
return <video ref={videoRef} controls />;
}Adaptive Bitrate
// Different quality levels for different connections
const qualityLevels = {
low: '/video-480p.mp4',
medium: '/video-720p.mp4',
high: '/video-1080p.mp4',
};
function AdaptiveVideo() {
const [quality, setQuality] = useState('medium');
useEffect(() => {
const connection = (navigator as any).connection;
if (connection) {
if (connection.effectiveType === '4g' && !connection.saveData) {
setQuality('high');
} else if (connection.effectiveType === '2g') {
setQuality('low');
}
}
}, []);
return <video src={qualityLevels[quality]} controls />;
}Video CDN Solutions
Self-hosting video is almost never the right choice for production. Video files are large, transcoding is CPU-intensive, and bandwidth costs add up fast. Use a dedicated video CDN.
Cloudinary Video
Cloudinary handles video transcoding, optimization, and delivery automatically:
const CLOUDINARY_CLOUD = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD;
export function getCloudinaryVideoUrl(
publicId: string,
options: {
width?: number;
quality?: 'auto' | number;
format?: 'auto' | 'mp4' | 'webm';
} = {}
) {
const { width, quality = 'auto', format = 'auto' } = options;
const transforms = [
width && `w_${width}`,
`q_${quality}`,
`f_${format}`,
'vc_auto', // Auto video codec selection
].filter(Boolean).join(',');
return `https://res.cloudinary.com/${CLOUDINARY_CLOUD}/video/upload/${transforms}/${publicId}`;
}// Usage with automatic format selection
function OptimizedVideo({ videoId, poster }) {
const videoUrl = getCloudinaryVideoUrl(videoId, {
width: 1280,
quality: 'auto'
});
return (
<video
poster={poster}
controls
preload="metadata"
style={{ aspectRatio: '16/9' }}
>
<source src={videoUrl} type="video/mp4" />
</video>
);
}Cloudinary Adaptive Streaming
For longer videos, use HLS streaming with automatic quality adaptation:
// Cloudinary generates HLS manifest automatically
function CloudinaryStreamingVideo({ publicId }) {
const hlsUrl = `https://res.cloudinary.com/${CLOUDINARY_CLOUD}/video/upload/sp_auto/${publicId}.m3u8`;
return <StreamingVideo src={hlsUrl} />;
}Alternative Video CDNs
| Provider | Best For | Pricing Model |
|---|---|---|
| Cloudinary | General media, good free tier | Pay-per-bandwidth + transforms |
| Mux | Professional video, analytics | Pay-per-minute stored/streamed |
| Bunny Stream | Budget-friendly | Flat bandwidth pricing |
| Cloudflare Stream | Simple hosting | Per-minute stored |
For detailed cost comparisons between self-hosting and CDN solutions, see the Cost Optimization Guide.
Audio Optimization
Lazy Load Audio
function AudioPlayer({ src }) {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div>
{!isLoaded ? (
<button onClick={() => setIsLoaded(true)}>
Load Audio Player
</button>
) : (
<audio controls src={src} />
)}
</div>
);
}Audio Formats
<audio controls>
<source src="/audio.opus" type="audio/opus" />
<source src="/audio.aac" type="audio/aac" />
<source src="/audio.mp3" type="audio/mpeg" />
</audio>Embedded Content
Lazy Load Iframes
// YouTube with facade pattern
function YouTubeEmbed({ videoId, title }) {
const [isLoaded, setIsLoaded] = useState(false);
if (!isLoaded) {
return (
<button
onClick={() => setIsLoaded(true)}
className="relative aspect-video w-full bg-black"
aria-label={`Play ${title}`}
>
<img
src={`https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`}
alt=""
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center">
<svg className="w-20 h-20 text-red-600" viewBox="0 0 68 48">
<path fill="currentColor" d="M66.52,7.74..." />
</svg>
</div>
</button>
);
}
return (
<iframe
src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
title={title}
className="aspect-video w-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
allowFullScreen
/>
);
}Third-Party Embed Optimization
// Use lite-youtube-embed for better performance
import 'lite-youtube-embed';
function LiteYouTube({ videoId }) {
return (
<lite-youtube
videoid={videoId}
playlabel="Play Video"
/>
);
}SVG Optimization
Inline SVGs
// For small, frequently used icons
function Icon() {
return (
<svg viewBox="0 0 24 24" className="w-6 h-6">
<path d="M12 2L2 7l10 5 10-5-10-5z" />
</svg>
);
}SVG Sprites
// Create sprite sheet
// public/icons.svg
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="..." />
</symbol>
<symbol id="icon-settings" viewBox="0 0 24 24">
<path d="..." />
</symbol>
</svg>
// Use in component
function Icon({ name }) {
return (
<svg className="w-6 h-6">
<use href={`/icons.svg#icon-${name}`} />
</svg>
);
}Performance Impact
| Media Type | Unoptimized | Optimized | Savings |
|---|---|---|---|
| Video (1min) | 50MB | 10MB | 80% |
| Background video | 20MB | 5MB | 75% |
| Audio (3min) | 5MB | 1MB | 80% |
| YouTube embed | 1.3MB initial | ~50KB | 96% |
Checklist
Video
Use a video CDN (Cloudinary, Mux, etc.) Don't self-host video. CDNs handle transcoding, streaming, and delivery.
Use WebM/AV1 with MP4 fallback Modern formats are significantly smaller than H.264.
Lazy load off-screen videos Use Intersection Observer to defer loading.
Add poster images Prevent CLS and improve perceived performance.
Use appropriate preload value Use "none" or "metadata" to save bandwidth.
Respect reduced motion preference Show static image for users who prefer no motion.
Embeds
Use facade/lite-embed patterns Load a thumbnail first, iframe on click.
Lazy load iframes Don't load embeds until they're visible.
Set explicit dimensions Prevent layout shift when embed loads.
Consider privacy implications Third-party embeds can track users.
Audio
Use modern formats (Opus, AAC) Better compression than MP3.
Lazy load audio players Load player on user interaction.
Compress appropriately Match bitrate to use case.
For bandwidth cost analysis and CDN pricing comparisons, see the Cost Optimization Guide.