Back to Blog
ImagesPerformanceWeb DevelopmentOptimization

Image Optimization for Web Performance

Optimize images for faster load times. From formats to compression to responsive images and lazy loading.

B
Bootspring Team
Engineering
April 12, 2024
5 min read

Images account for most of a page's weight. Proper optimization dramatically improves load times without sacrificing visual quality.

Image Formats#

JPEG - Best for: Photographs - Lossy compression - No transparency - Wide support PNG - Best for: Graphics, screenshots - Lossless compression - Supports transparency - Larger file sizes WebP - Best for: Modern browsers - 25-35% smaller than JPEG - Supports transparency - 95%+ browser support AVIF - Best for: Maximum compression - 50% smaller than JPEG - Slower encoding - Growing browser support (~90%) SVG - Best for: Icons, logos - Vector format (scales infinitely) - Can be styled with CSS - Inline-able in HTML

Format Selection#

1<!-- Progressive enhancement with picture --> 2<picture> 3 <source srcset="image.avif" type="image/avif"> 4 <source srcset="image.webp" type="image/webp"> 5 <img src="image.jpg" alt="Description"> 6</picture>

Responsive Images#

1<!-- Different sizes for different viewports --> 2<img 3 src="hero-800.jpg" 4 srcset=" 5 hero-400.jpg 400w, 6 hero-800.jpg 800w, 7 hero-1200.jpg 1200w, 8 hero-1600.jpg 1600w 9 " 10 sizes=" 11 (max-width: 600px) 100vw, 12 (max-width: 1200px) 50vw, 13 800px 14 " 15 alt="Hero image" 16> 17 18<!-- Art direction with picture --> 19<picture> 20 <source 21 media="(min-width: 1024px)" 22 srcset="hero-desktop.jpg" 23 > 24 <source 25 media="(min-width: 768px)" 26 srcset="hero-tablet.jpg" 27 > 28 <img src="hero-mobile.jpg" alt="Hero"> 29</picture> 30 31<!-- High DPI screens --> 32<img 33 src="logo.png" 34 srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x" 35 alt="Logo" 36>

Lazy Loading#

1<!-- Native lazy loading --> 2<img src="image.jpg" loading="lazy" alt="Description"> 3 4<!-- With placeholder --> 5<img 6 src="placeholder.jpg" 7 data-src="image.jpg" 8 class="lazyload" 9 alt="Description" 10>
1// Intersection Observer for custom lazy loading 2class LazyLoader { 3 private observer: IntersectionObserver; 4 5 constructor() { 6 this.observer = new IntersectionObserver( 7 (entries) => { 8 entries.forEach((entry) => { 9 if (entry.isIntersecting) { 10 this.loadImage(entry.target as HTMLImageElement); 11 this.observer.unobserve(entry.target); 12 } 13 }); 14 }, 15 { 16 rootMargin: '50px 0px', // Load 50px before visible 17 } 18 ); 19 } 20 21 observe(images: NodeListOf<HTMLImageElement>) { 22 images.forEach((img) => this.observer.observe(img)); 23 } 24 25 private loadImage(img: HTMLImageElement) { 26 const src = img.dataset.src; 27 const srcset = img.dataset.srcset; 28 29 if (src) img.src = src; 30 if (srcset) img.srcset = srcset; 31 32 img.classList.remove('lazyload'); 33 img.classList.add('lazyloaded'); 34 } 35} 36 37// Usage 38const lazyLoader = new LazyLoader(); 39lazyLoader.observe(document.querySelectorAll('img.lazyload'));

Compression Tools#

1# Sharp (Node.js) 2npm install sharp 3 4# ImageMagick 5brew install imagemagick 6 7# Squoosh CLI 8npm install @squoosh/cli
1// Sharp for Node.js 2import sharp from 'sharp'; 3 4async function optimizeImage( 5 inputPath: string, 6 outputPath: string, 7 options: { 8 width?: number; 9 quality?: number; 10 format?: 'jpeg' | 'webp' | 'avif' | 'png'; 11 } 12) { 13 let image = sharp(inputPath); 14 15 if (options.width) { 16 image = image.resize(options.width); 17 } 18 19 switch (options.format) { 20 case 'jpeg': 21 image = image.jpeg({ quality: options.quality || 80 }); 22 break; 23 case 'webp': 24 image = image.webp({ quality: options.quality || 80 }); 25 break; 26 case 'avif': 27 image = image.avif({ quality: options.quality || 65 }); 28 break; 29 case 'png': 30 image = image.png({ compressionLevel: 9 }); 31 break; 32 } 33 34 await image.toFile(outputPath); 35} 36 37// Generate multiple sizes 38async function generateResponsiveImages(inputPath: string) { 39 const sizes = [400, 800, 1200, 1600]; 40 const formats = ['webp', 'jpg'] as const; 41 42 for (const size of sizes) { 43 for (const format of formats) { 44 const outputPath = inputPath.replace( 45 /\.[^.]+$/, 46 `-${size}.${format}` 47 ); 48 49 await optimizeImage(inputPath, outputPath, { 50 width: size, 51 format: format === 'jpg' ? 'jpeg' : format, 52 quality: 80, 53 }); 54 } 55 } 56}

Next.js Image Component#

1import Image from 'next/image'; 2 3// Automatic optimization 4<Image 5 src="/hero.jpg" 6 alt="Hero" 7 width={1200} 8 height={600} 9 priority // Load immediately for LCP 10/> 11 12// Fill container 13<div style={{ position: 'relative', width: '100%', height: '400px' }}> 14 <Image 15 src="/hero.jpg" 16 alt="Hero" 17 fill 18 style={{ objectFit: 'cover' }} 19 sizes="100vw" 20 /> 21</div> 22 23// External images 24// next.config.js 25module.exports = { 26 images: { 27 remotePatterns: [ 28 { 29 protocol: 'https', 30 hostname: 'images.example.com', 31 }, 32 ], 33 }, 34};

Placeholder Strategies#

1// Blur placeholder 2<Image 3 src="/large-image.jpg" 4 alt="Description" 5 width={800} 6 height={600} 7 placeholder="blur" 8 blurDataURL="data:image/jpeg;base64,/9j/4AAQ..." // Low-quality image 9/> 10 11// Dominant color placeholder 12const dominantColor = await getDominantColor(imagePath); 13 14<div style={{ backgroundColor: dominantColor }}> 15 <Image src="/image.jpg" alt="Description" /> 16</div> 17 18// LQIP (Low Quality Image Placeholder) 19<img 20 src="image-lqip.jpg" 21 data-src="image-full.jpg" 22 class="lazyload blur-up" 23 alt="Description" 24>
1/* Blur up effect */ 2.blur-up { 3 filter: blur(10px); 4 transition: filter 0.3s; 5} 6 7.blur-up.lazyloaded { 8 filter: blur(0); 9}

SVG Optimization#

# SVGO for SVG optimization npm install svgo # Run optimization npx svgo input.svg -o output.svg
1// svgo.config.js 2module.exports = { 3 plugins: [ 4 'preset-default', 5 'removeDimensions', 6 { 7 name: 'removeAttrs', 8 params: { 9 attrs: ['data-*', 'class'], 10 }, 11 }, 12 ], 13};
1// Inline SVG as React component 2function Icon({ name }: { name: string }) { 3 return ( 4 <svg aria-hidden="true" className="icon"> 5 <use href={`/icons.svg#${name}`} /> 6 </svg> 7 ); 8}

Performance Checklist#

1## Image Audit 2 3- [ ] Using modern formats (WebP, AVIF) 4- [ ] Responsive images with srcset/sizes 5- [ ] Lazy loading non-critical images 6- [ ] Proper dimensions (no layout shift) 7- [ ] Compressed appropriately 8- [ ] Correct aspect ratios 9- [ ] Alt text for accessibility 10- [ ] Preloading LCP image
1<!-- Preload LCP image --> 2<link 3 rel="preload" 4 as="image" 5 href="/hero.webp" 6 type="image/webp" 7>

Monitoring#

1// Track image loading performance 2function measureImagePerformance() { 3 const images = document.querySelectorAll('img'); 4 5 images.forEach((img) => { 6 if (img.complete) { 7 logImageMetrics(img); 8 } else { 9 img.addEventListener('load', () => logImageMetrics(img)); 10 } 11 }); 12} 13 14function logImageMetrics(img: HTMLImageElement) { 15 const entry = performance.getEntriesByName(img.src)[0] as PerformanceResourceTiming; 16 17 if (entry) { 18 console.log({ 19 src: img.src, 20 duration: entry.duration, 21 transferSize: entry.transferSize, 22 decodedBodySize: entry.decodedBodySize, 23 }); 24 } 25}

Conclusion#

Image optimization is one of the highest-impact performance improvements you can make. Use modern formats, serve responsive sizes, lazy load below-the-fold images, and preload critical ones.

Your users on slow connections will thank you.

Share this article

Help spread the word about Bootspring