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/cli1// 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.svg1// 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 image1<!-- 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.