Security & Performance
Image and Media Optimization
Image and Media Optimization
Images and media files often account for 50-70% of page weight. Optimizing visual assets is critical for performance, especially on mobile networks. This lesson covers modern image formats, responsive delivery techniques, lazy loading strategies, and video optimization to dramatically reduce bandwidth usage while maintaining visual quality.
Modern Image Formats
Next-generation formats provide superior compression:
<!-- Progressive enhancement with fallbacks -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
<!-- Format comparison (same quality) -->
JPEG: 100 KB
PNG: 150 KB
WebP: 40 KB (60% smaller than JPEG)
AVIF: 25 KB (75% smaller than JPEG)
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
<!-- Format comparison (same quality) -->
JPEG: 100 KB
PNG: 150 KB
WebP: 40 KB (60% smaller than JPEG)
AVIF: 25 KB (75% smaller than JPEG)
Note: WebP has 96% browser support (2024). AVIF has 85% support and offers even better compression. Always provide JPEG/PNG fallbacks for older browsers.
WebP and AVIF Benefits
/* WebP advantages */
- 25-35% smaller than JPEG at same quality
- Supports transparency (like PNG)
- Supports animation (like GIF)
- Lossy and lossless compression
/* AVIF advantages */
- 50% smaller than JPEG at same quality
- Superior compression algorithm (AV1)
- Better color accuracy
- HDR support
/* Browser support detection */
function supportsWebP() {
const canvas = document.createElement('canvas');
if (canvas.getContext && canvas.getContext('2d')) {
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
return false;
}
// Server-side detection (Accept header)
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
- 25-35% smaller than JPEG at same quality
- Supports transparency (like PNG)
- Supports animation (like GIF)
- Lossy and lossless compression
/* AVIF advantages */
- 50% smaller than JPEG at same quality
- Superior compression algorithm (AV1)
- Better color accuracy
- HDR support
/* Browser support detection */
function supportsWebP() {
const canvas = document.createElement('canvas');
if (canvas.getContext && canvas.getContext('2d')) {
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
return false;
}
// Server-side detection (Accept header)
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Responsive Images
Serve appropriately sized images for different devices:
<!-- Resolution switching with srcset -->
<img
src="image-800.jpg"
srcset="image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="Responsive image"
loading="lazy">
<!-- Art direction with picture -->
<picture>
<source media="(min-width: 1200px)" srcset="hero-desktop.jpg">
<source media="(min-width: 768px)" srcset="hero-tablet.jpg">
<img src="hero-mobile.jpg" alt="Hero image">
</picture>
<!-- Device pixel ratio targeting -->
<img
src="image.jpg"
srcset="image.jpg 1x,
image@2x.jpg 2x,
image@3x.jpg 3x"
alt="High DPI image">
<img
src="image-800.jpg"
srcset="image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="Responsive image"
loading="lazy">
<!-- Art direction with picture -->
<picture>
<source media="(min-width: 1200px)" srcset="hero-desktop.jpg">
<source media="(min-width: 768px)" srcset="hero-tablet.jpg">
<img src="hero-mobile.jpg" alt="Hero image">
</picture>
<!-- Device pixel ratio targeting -->
<img
src="image.jpg"
srcset="image.jpg 1x,
image@2x.jpg 2x,
image@3x.jpg 3x"
alt="High DPI image">
Tip: Use the `sizes` attribute to tell the browser the image display size. This allows it to select the optimal image from `srcset` before CSS is parsed, improving performance.
Lazy Loading Images
Defer loading of off-screen images:
<!-- Native lazy loading (recommended) -->
<img src="image.jpg" loading="lazy" alt="Lazy loaded image">
<!-- Eager loading for above-the-fold images -->
<img src="hero.jpg" loading="eager" alt="Hero image">
<!-- JavaScript Intersection Observer fallback -->
<img data-src="image.jpg" class="lazy" alt="Image">
<script>
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
</script>
<!-- Low quality image placeholder (LQIP) -->
<img
src="tiny-placeholder.jpg"
data-src="full-image.jpg"
class="lazy blur"
alt="Image with placeholder">
<img src="image.jpg" loading="lazy" alt="Lazy loaded image">
<!-- Eager loading for above-the-fold images -->
<img src="hero.jpg" loading="eager" alt="Hero image">
<!-- JavaScript Intersection Observer fallback -->
<img data-src="image.jpg" class="lazy" alt="Image">
<script>
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
</script>
<!-- Low quality image placeholder (LQIP) -->
<img
src="tiny-placeholder.jpg"
data-src="full-image.jpg"
class="lazy blur"
alt="Image with placeholder">
Warning: Don't lazy load above-the-fold images or LCP (Largest Contentful Paint) candidates. This harms Core Web Vitals and delays critical content visibility.
Image Compression Techniques
/* Compression strategies */
1. Lossy compression (JPEG, WebP lossy)
- Reduces file size by removing visual data
- Quality 80-85 is optimal (human eye can't tell difference)
- Tools: ImageOptim, Squoosh, TinyPNG
2. Lossless compression (PNG, WebP lossless)
- Reduces file size without quality loss
- Best for graphics, logos, icons
- Tools: OptiPNG, PNGQuant, ImageOptim
3. Responsive compression
- Higher compression for mobile
- Lower compression for desktop/retina
/* Command-line optimization */
// JPEG optimization
jpegoptim --max=85 --strip-all --all-progressive image.jpg
// PNG optimization
optipng -o7 image.png
pngquant --quality=65-80 image.png
// WebP conversion
cwebp -q 80 input.jpg -o output.webp
// AVIF conversion
avifenc --min 20 --max 25 input.jpg output.avif
1. Lossy compression (JPEG, WebP lossy)
- Reduces file size by removing visual data
- Quality 80-85 is optimal (human eye can't tell difference)
- Tools: ImageOptim, Squoosh, TinyPNG
2. Lossless compression (PNG, WebP lossless)
- Reduces file size without quality loss
- Best for graphics, logos, icons
- Tools: OptiPNG, PNGQuant, ImageOptim
3. Responsive compression
- Higher compression for mobile
- Lower compression for desktop/retina
/* Command-line optimization */
// JPEG optimization
jpegoptim --max=85 --strip-all --all-progressive image.jpg
// PNG optimization
optipng -o7 image.png
pngquant --quality=65-80 image.png
// WebP conversion
cwebp -q 80 input.jpg -o output.webp
// AVIF conversion
avifenc --min 20 --max 25 input.jpg output.avif
Automated Image Optimization
// Next.js Image component (automatic optimization)
import Image from 'next/image';
export default function Gallery() {
return (
<Image
src="/photo.jpg"
width={800}
height={600}
alt="Photo"
loading="lazy"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// Automatically serves WebP/AVIF, resizes, lazy loads
// Webpack image optimization
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
module.exports = {
plugins: [
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
['imagemin-webp', { quality: 80 }],
['imagemin-mozjpeg', { quality: 85 }],
],
},
},
}),
],
};
import Image from 'next/image';
export default function Gallery() {
return (
<Image
src="/photo.jpg"
width={800}
height={600}
alt="Photo"
loading="lazy"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// Automatically serves WebP/AVIF, resizes, lazy loads
// Webpack image optimization
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
module.exports = {
plugins: [
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
['imagemin-webp', { quality: 80 }],
['imagemin-mozjpeg', { quality: 85 }],
],
},
},
}),
],
};
Note: Image optimization should happen at build time, not runtime. Use build tools or image CDNs (Cloudinary, Imgix, ImageKit) for automatic optimization and delivery.
Video Optimization
Optimize video delivery for performance:
<!-- Multiple formats with fallback -->
<video controls preload="metadata" poster="thumbnail.jpg">
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
Your browser doesn't support video.
</video>
<!-- Lazy loading video -->
<video preload="none" poster="poster.jpg">
<source data-src="video.mp4" type="video/mp4">
</video>
<script>
const videoObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const video = entry.target;
const source = video.querySelector('source');
source.src = source.dataset.src;
video.load();
videoObserver.unobserve(video);
}
});
});
document.querySelectorAll('video').forEach(video => {
videoObserver.observe(video);
});
</script>
<!-- Replace GIF with video -->
<!-- Before: animated.gif (2 MB) -->
<img src="animated.gif" alt="Animation">
<!-- After: video (200 KB, 90% smaller) -->
<video autoplay loop muted playsinline>
<source src="animated.webm" type="video/webm">
<source src="animated.mp4" type="video/mp4">
</video>
<video controls preload="metadata" poster="thumbnail.jpg">
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
Your browser doesn't support video.
</video>
<!-- Lazy loading video -->
<video preload="none" poster="poster.jpg">
<source data-src="video.mp4" type="video/mp4">
</video>
<script>
const videoObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const video = entry.target;
const source = video.querySelector('source');
source.src = source.dataset.src;
video.load();
videoObserver.unobserve(video);
}
});
});
document.querySelectorAll('video').forEach(video => {
videoObserver.observe(video);
});
</script>
<!-- Replace GIF with video -->
<!-- Before: animated.gif (2 MB) -->
<img src="animated.gif" alt="Animation">
<!-- After: video (200 KB, 90% smaller) -->
<video autoplay loop muted playsinline>
<source src="animated.webm" type="video/webm">
<source src="animated.mp4" type="video/mp4">
</video>
Video Compression Best Practices
/* Video optimization strategies */
1. Choose appropriate codec
- H.264 (MP4): Best compatibility
- VP9 (WebM): Better compression, 94% support
- AV1: Future codec, 72% support, 50% smaller
2. Adaptive bitrate streaming
- Serve different qualities based on bandwidth
- Use HLS or DASH protocols
3. Compression settings
- CRF (Constant Rate Factor): 23-28 for web
- Two-pass encoding for better quality
- Remove audio if not needed
/* FFmpeg compression commands */
// Convert to WebM (VP9)
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 output.webm
// Convert to MP4 (H.264)
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset slow output.mp4
// Remove audio
ffmpeg -i input.mp4 -c:v copy -an output.mp4
// Resize video
ffmpeg -i input.mp4 -vf scale=1280:-1 output.mp4
1. Choose appropriate codec
- H.264 (MP4): Best compatibility
- VP9 (WebM): Better compression, 94% support
- AV1: Future codec, 72% support, 50% smaller
2. Adaptive bitrate streaming
- Serve different qualities based on bandwidth
- Use HLS or DASH protocols
3. Compression settings
- CRF (Constant Rate Factor): 23-28 for web
- Two-pass encoding for better quality
- Remove audio if not needed
/* FFmpeg compression commands */
// Convert to WebM (VP9)
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 output.webm
// Convert to MP4 (H.264)
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset slow output.mp4
// Remove audio
ffmpeg -i input.mp4 -c:v copy -an output.mp4
// Resize video
ffmpeg -i input.mp4 -vf scale=1280:-1 output.mp4
Tip: Use streaming services like YouTube, Vimeo, or Cloudflare Stream for large videos. They handle optimization, adaptive bitrate, and global CDN delivery automatically.
CDN and Image Services
/* Image CDN URL parameters */
// Cloudinary transformations
https://res.cloudinary.com/demo/image/upload/
w_800,h_600,c_fill,q_auto,f_auto/sample.jpg
// Auto format, quality, and resize
// Imgix parameters
https://demo.imgix.net/photo.jpg?
w=800&h=600&fit=crop&auto=format,compress
// ImageKit transformations
https://ik.imagekit.io/demo/photo.jpg?
tr=w-800,h-600,fo-auto,q-80
/* Benefits of image CDNs */
- Automatic format detection and conversion
- On-the-fly resizing and cropping
- Smart compression based on device
- Global CDN delivery
- Cache optimization
- Image analytics
// Cloudinary transformations
https://res.cloudinary.com/demo/image/upload/
w_800,h_600,c_fill,q_auto,f_auto/sample.jpg
// Auto format, quality, and resize
// Imgix parameters
https://demo.imgix.net/photo.jpg?
w=800&h=600&fit=crop&auto=format,compress
// ImageKit transformations
https://ik.imagekit.io/demo/photo.jpg?
tr=w-800,h-600,fo-auto,q-80
/* Benefits of image CDNs */
- Automatic format detection and conversion
- On-the-fly resizing and cropping
- Smart compression based on device
- Global CDN delivery
- Cache optimization
- Image analytics
Performance Metrics
// Measure image loading performance
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'img') {
console.log('Image:', entry.name);
console.log('Size:', entry.transferSize, 'bytes');
console.log('Duration:', entry.duration, 'ms');
}
}
});
observer.observe({ entryTypes: ['resource'] });
// Calculate LCP for images
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'img') {
console.log('Image:', entry.name);
console.log('Size:', entry.transferSize, 'bytes');
console.log('Duration:', entry.duration, 'ms');
}
}
});
observer.observe({ entryTypes: ['resource'] });
// Calculate LCP for images
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
Exercise: Audit your site's images using Chrome DevTools Network tab. Calculate total image weight and identify the largest files. Convert at least 5 images to WebP/AVIF format and implement responsive image markup with srcset. Add native lazy loading to below-the-fold images. Replace any animated GIFs with video elements. Measure the impact on page weight and load time using Lighthouse and WebPageTest.