Image optimization is the single most impactful performance work you can do. Images are typically 50-70% of total page weight, and the fix is straightforward: serve the right format, at the right size, at the right time.
Choosing the right format
| Format | Best for | Size vs JPEG | Browser support |
|---|---|---|---|
| AVIF | Photos, complex images | ~50% smaller | Chrome, Firefox, Safari 16.4+ |
| WebP | Photos, complex images | ~30% smaller | All modern browsers |
| JPEG | Photos (legacy fallback) | Baseline | Universal |
| PNG | Images with transparency | 2-10x larger | Universal |
| SVG | Icons, logos, illustrations | Tiny (vector) | Universal |
The <picture> element lets you offer modern formats with automatic fallbacks:
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>The browser picks the first format it supports. Modern browsers get AVIF (smallest), older browsers get WebP, and ancient browsers get JPEG.
<img src="photo.jpg"> tags. It never uses <picture>, srcset, or width/height attributes. Every image tag AI produces needs manual optimization.Format decision table
| Image type | Recommended format | Why |
|---|---|---|
| Hero photo | AVIF with WebP and JPEG fallback | Maximum compression for the largest asset |
| Product photo | WebP with JPEG fallback | Good balance of quality and compression |
| Icon or logo | SVG | Scales perfectly, tiny file size |
| Screenshot with text | WebP or PNG | Sharp text rendering |
| Transparent image | WebP (supports alpha) or PNG | JPEG does not support transparency |
Responsive images
Sending a 4000x3000 image to a 400px phone wastes ~90% of bandwidthWhat is bandwidth?How much data can flow through a connection at once - like the number of lanes on a highway rather than the speed limit.. The srcset attribute tells the browser which sizes are available:
<img
srcset="
product-400w.webp 400w,
product-800w.webp 800w,
product-1200w.webp 1200w
"
sizes="
(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px
"
src="product-800w.webp"
alt="Product photo"
width="800"
height="600"
/>The browser combines sizes with the device pixel ratio to pick the optimal file from srcset. On a 2x display with a 400px viewportWhat is viewport?The visible area of a web page in the browser window. Its size changes depending on the device and whether the user has resized the window., it fetches the 800w image.
Loading strategies for images
<!-- LCP image: load immediately with highest priority -->
<img src="hero.webp" loading="eager" fetchpriority="high"
width="1200" height="600" alt="Hero" />
<!-- Above-fold but not LCP: normal priority -->
<img src="logo.svg" width="200" height="50" alt="Logo" />
<!-- Below-fold: lazy load -->
<img src="product.webp" loading="lazy"
width="400" height="300" alt="Product" />| Image type | loading | fetchpriority | Why |
|---|---|---|---|
| LCP / Hero | eager | high | Directly affects your LCP score |
| Above-fold secondary | default | default | Important but not critical |
| Below-fold content | lazy | default | User has not scrolled here yet |
| Decorative / background | lazy | low | Least important |
Image compression
| Tool | Type | Notes |
|---|---|---|
| Squoosh (squoosh.app) | Manual, browser-based | Best for one-off optimization |
| Sharp (npm package) | Programmatic | Build-time image processing |
| Cloudflare Images | CDN service | Automatic format/size on the edge |
| Imgix / Cloudinary | CDN service | URL-based transformations |
// scripts/optimize-images.js
import sharp from 'sharp';
async function optimizeImage(input, output) {
await sharp(input)
.resize(1200, 600, { fit: 'cover' })
.webp({ quality: 80 })
.toFile(output);
}
// Generates a 1200x600 WebP at 80% quality
// A 5MB JPEG becomes ~100KB WebPCompression quality guidelines
| Use case | WebP quality | AVIF quality | Visual result |
|---|---|---|---|
| Hero / portfolio | 85-90 | 70-80 | Near-lossless |
| Product photos | 75-85 | 60-70 | Good, no visible artifacts |
| Thumbnails | 60-75 | 45-60 | Acceptable, small display hides artifacts |
| Decorative backgrounds | 50-65 | 35-50 | Lower quality is fine when blurred or overlaid |
Image CDNs
An image CDNWhat is cdn?Content Delivery Network - a network of servers around the world that caches your files and serves them from the location closest to the user, making pages load faster. automatically serves images in the optimal format and size based on the requesting device:
<!-- Cloudflare Image Resizing -->
<img src="/cdn-cgi/image/width=800,format=auto/original.jpg" alt="Product" />
<!-- Imgix -->
<img src="https://your-domain.imgix.net/photo.jpg?w=800&auto=format" alt="Product" />The format=auto parameter means the CDN detects the browser's supported formats and serves AVIF, WebP, or JPEG accordingly.
| Approach | Pros | Cons |
|---|---|---|
| Manual with Sharp | Full control, free, works offline | More setup, manual process |
| Image CDN | Automatic, handles all devices | Monthly cost, vendor dependency |
| Framework built-in (Next.js Image) | Integrated, easy to use | Framework-specific |
Preventing CLSWhat is cls?Cumulative Layout Shift - measures how much the page layout moves around unexpectedly while loading, with a target score under 0.1. from images
Every image must have explicit dimensions so the browser can reserve space before it loads:
<!-- Bad: browser doesn't know how tall this will be -->
<img src="photo.jpg" alt="Photo" />
<!-- Good: browser reserves 600px of height immediately -->
<img src="photo.jpg" width="800" height="600" alt="Photo" />
<!-- Also good: aspect-ratio in CSS -->
<img src="photo.jpg" alt="Photo"
style="aspect-ratio: 4/3; width: 100%; height: auto;" /><img> tags without dimensions, lazy loading, or responsive sizes. A gallery with 20 product images at 2MB each downloads 40MB on page load. With proper optimization, the same gallery might transfer 2MB total.Quick reference
| Optimization | Impact | Effort | When to use |
|---|---|---|---|
| Convert to WebP/AVIF | Very high (30-50% size reduction) | Low | Always |
| Responsive srcset | High (up to 90% savings on mobile) | Medium | Any image viewed at different sizes |
| Lazy loading | Medium (defers non-critical loads) | Low | All below-fold images |
| Image CDN | Very high (automated everything) | Low (setup cost) | Production sites with many images |
| Compression | High (50-80% reduction) | Low | Always |
| Explicit dimensions | Prevents CLS | Low | Every single image |
// Image optimization patterns
// 1. React component with full optimization
function OptimizedImage({ src, alt, width, height, priority = false }) {
return (
<picture>
<source
srcSet={`${src}.avif`}
type="image/avif"
/>
<source
srcSet={`${src}.webp`}
type="image/webp"
/>
<img
srcSet={`
${src}-400w.jpg 400w,
${src}-800w.jpg 800w,
${src}-1200w.jpg 1200w
`}
sizes="(max-width: 600px) 100vw, 800px"
src={`${src}-800w.jpg`}
alt={alt}
width={width}
height={height}
loading={priority ? 'eager' : 'lazy'}
fetchpriority={priority ? 'high' : undefined}
/>
</picture>
);
}
// 2. Build-time optimization with Sharp
import sharp from 'sharp';
import { glob } from 'glob';
async function optimizeAll() {
const images = await glob('src/images/**/*.{jpg,png}');
for (const img of images) {
// Generate WebP version
await sharp(img)
.resize(1200)
.webp({ quality: 80 })
.toFile(img.replace(/\.(jpg|png)$/, '.webp'));
// Generate AVIF version
await sharp(img)
.resize(1200)
.avif({ quality: 65 })
.toFile(img.replace(/\.(jpg|png)$/, '.avif'));
}
}