Images account for roughly 50% of total page weight on the average website. A page with ten unoptimized 2MB product photos loads 20MB of image data alone — enough to blow LCP (Largest Contentful Paint) past 4 seconds on a 4G connection and destroy your Core Web Vitals score.
The good news: image optimization is high-leverage. Converting to modern formats, serving responsive sizes, and lazy loading below-the-fold images can reduce image bandwidth by 60-80% without visible quality loss. The techniques are well-understood and the tooling is mature.
This guide covers the complete optimization stack: format selection, quality tuning, responsive images with srcset, the picture element for format switching, lazy loading, critical image preloading, image CDNs, and build-time optimization pipelines. The goal is zero wasted bytes without sacrificing visual quality.
Why It Matters: Core Web Vitals and LCP
Google's Core Web Vitals use LCP (Largest Contentful Paint) as a key metric. LCP measures when the largest visible element finishes rendering — and on most pages, that's an image. Google considers LCP under 2.5 seconds "good" and over 4 seconds "poor."
Images affect LCP through three channels:
- Download time: A 500KB hero image takes 1 second on a 4Mbps connection. A 2MB image takes 4 seconds. Format and quality optimization directly reduce download time.
- Decode time: The browser must decompress the image data into pixels. AVIF decodes slower than JPEG. Very large images (4000x3000 at 32-bit) consume significant memory and decode time. Serving appropriately sized images reduces decode overhead.
- Render blocking: If images aren't lazy-loaded, the browser downloads all images immediately — even those below the fold — competing for bandwidth with the LCP image. Lazy loading non-critical images frees bandwidth for the hero.
A well-optimized image pipeline is often the fastest path to passing Core Web Vitals.
Step 1: Choose the Right Format
Format selection is the highest-impact optimization. The same image at the same visual quality can vary 3-5x in file size depending on format.
Format Decision Tree
- Photograph, no transparency: AVIF (50% smaller than JPEG) → WebP fallback (25-35% smaller) → JPEG baseline
- Photograph with transparency: WebP lossy + lossless alpha → PNG
- Screenshot or UI graphic: WebP lossless → PNG
- Logo or icon: SVG (vector) → PNG (if raster needed)
- Simple animation: MP4 (video) → animated WebP → GIF (only for email)
- Charts and diagrams: SVG → PNG (if vector not possible)
The three-format stack (AVIF + WebP + JPEG) using the <picture> element gives you maximum compression for modern browsers with guaranteed compatibility for older ones.
Quality Settings by Format
| Format | General Web | Hero/Product Images | Thumbnails |
|---|---|---|---|
| JPEG (mozjpeg) | 82-87 | 88-92 | 70-80 |
| WebP lossy | 75-80 | 82-88 | 60-72 |
| AVIF | 55-65 (CRF 32-38) | 65-75 (CRF 26-32) | 45-55 (CRF 38-44) |
These ranges produce visually indistinguishable results from the source at normal viewing distances. Always evaluate quality visually on representative images rather than trusting numbers — different content responds differently to compression.
Step 2: Responsive Images (srcset and sizes)
A 2400px-wide hero image is overkill for a phone with a 390px viewport (even at 3x = 1170px). Serving a single large image to all devices wastes bandwidth on mobile and loads slowly. Responsive images solve this by letting the browser choose the right size.
srcset and sizes Attributes
<img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w, hero-1600.jpg 1600w, hero-2400.jpg 2400w" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw" alt="Hero image">
The srcset attribute lists available image files with their pixel widths. The sizes attribute tells the browser how wide the image will be displayed at each viewport size. The browser combines these to choose the smallest file that covers the display area at the device's pixel density.
On a 390px phone at 3x density, the browser needs at least 1170px of image data. It picks hero-1200.jpg. On a 1440px desktop at 1x, displaying at 50vw (720px), it picks hero-800.jpg. Each device gets exactly what it needs.
The sizes attribute is critical. Without it, the browser assumes the image is 100vw (full viewport width) and downloads the largest file for high-density screens. Getting sizes right can reduce image downloads by 50-70% on mobile.
The picture Element for Format Switching
The <picture> element lets you serve different formats to different browsers:
<picture><source srcset="hero.avif" type="image/avif"><source srcset="hero.webp" type="image/webp"><img src="hero.jpg" alt="Hero"></picture>
Browsers download only the first source they support. Chrome downloads the AVIF (smallest), Safari 15 downloads WebP, and IE11 (if anyone still uses it) downloads the JPEG. Zero overhead — no unnecessary downloads, no JavaScript.
Combine <picture> with srcset for both format switching and responsive sizing:
<picture><source srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w" sizes="100vw" type="image/avif"><source srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="100vw" type="image/webp"><img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w" sizes="100vw" alt="Hero"></picture>
Step 3: Lazy Loading
Lazy loading defers the download of below-the-fold images until the user scrolls near them. This reduces initial page load time and bandwidth consumption.
Native lazy loading: <img src="photo.jpg" loading="lazy" alt="...">. Supported in all modern browsers (Chrome, Firefox, Safari, Edge). The browser decides when to start loading based on scroll position and connection speed. No JavaScript required.
Do NOT lazy load above-the-fold images. The LCP image (hero, main product photo) should load immediately. Adding loading="lazy" to the LCP image delays it, hurting Core Web Vitals. Only lazy load images that are below the initial viewport.
Placeholder strategies:
- Dominant color placeholder: Set a
background-colormatching the image's dominant color. Instant rendering, 0 bytes, smooth visual transition. - LQIP (Low Quality Image Placeholder): A tiny (20-40 byte) base64-encoded blurred thumbnail shown while the full image loads. Creates a "blur-up" effect. BlurHash and ThumbHash are popular encoding formats.
- Aspect ratio preservation: Set
widthandheightattributes (or CSSaspect-ratio) to prevent layout shift (CLS) when the image loads. This is independent of lazy loading and should always be done.
Step 4: Preload Critical Images
The LCP image should load as fast as possible. Preloading tells the browser to start downloading it immediately, before the HTML parser encounters the <img> tag.
<link rel="preload" as="image" href="hero.webp" type="image/webp" fetchpriority="high">
The fetchpriority="high" attribute (supported in Chrome and Edge) further prioritizes the image download over other resources. For the LCP image, this combination — preload + fetchpriority="high" — can reduce LCP by 200-500ms.
Preload with responsive images:
<link rel="preload" as="image" imagesrcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" imagesizes="100vw" type="image/webp">
Only preload 1-2 images per page. Preloading too many images defeats the purpose — everything becomes high priority, which means nothing is.
Step 5: CDN and Image Transformation
Image CDNs serve images from edge locations close to users and can transform images on-the-fly — resizing, converting formats, and adjusting quality based on the requesting device.
Image CDN Services
Cloudflare Image Resizing: Transform images via URL parameters. cdn-cgi/image/width=800,quality=80,format=auto/image.jpg. Format auto-detection serves AVIF to Chrome, WebP to Safari, JPEG to older browsers. Available on Pro+ plans.
Cloudinary: Full image management platform. URL-based transformations (resize, crop, effects), automatic format and quality selection, responsive breakpoint generation. Free tier: 25GB bandwidth/month.
imgix: Real-time image processing via URL parameters. Excellent documentation, fast global CDN, automatic format negotiation via auto=format parameter.
Vercel Image Optimization: Built into Next.js via the next/image component. Automatic format selection, responsive sizing, lazy loading, and blur placeholders.
The advantage of image CDNs: you upload one high-quality source image, and the CDN generates all the variants (sizes, formats, qualities) on demand. This eliminates the build-time image generation step entirely.
Content Negotiation (Accept Header)
Instead of using the <picture> element, some setups use server-side content negotiation. The browser sends an Accept header listing supported formats. The server (or CDN) returns the best format automatically.
Chrome sends: Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*;q=0.8
The server sees AVIF in the Accept header and returns the AVIF version. No HTML changes needed — a single <img> tag serves the optimal format to every browser. Cloudflare, Cloudinary, and imgix all support this via their "auto format" features.
Step 6: Build-Time Optimization Pipeline
For static sites and SSG frameworks, optimize images at build time rather than runtime.
A typical build-time pipeline:
- Source images in a content directory (full resolution, maximum quality)
- Sharp (libvips) generates responsive variants: 400w, 800w, 1200w, 1600w, 2400w
- Each variant is encoded in three formats: AVIF (speed 6), WebP (method 4), JPEG (mozjpeg quality 85)
- BlurHash or dominant color placeholders are generated
- The build tool generates
<picture>elements with correct srcset and sizes
Tools:
- Sharp: The fastest Node.js image processing library (backed by libvips). Handles resize, format conversion, and quality tuning. 5-10x faster than ImageMagick.
- Astro Image: Astro's built-in image component handles optimization automatically.
- Next.js Image: Automatic optimization with
next/image— generates responsive variants and handles lazy loading. - Squoosh CLI: Google's CLI image optimizer. Good for one-off batch optimization.
Common Optimization Mistakes
Serving 4K images to mobile devices. A 3840x2160 hero image at quality 85 is 2-4MB. A phone displaying it at 390px wide (1170px at 3x) only needs a 1200px-wide image at 200-400KB. Use srcset to serve the right size.
Lazy loading the LCP image. The biggest above-the-fold image should have loading="eager" (or no loading attribute) and optionally fetchpriority="high". Lazy loading it delays LCP by the intersection observer threshold + download time.
Missing width and height attributes. Without explicit dimensions, the browser can't reserve space for the image before it loads, causing layout shift (bad CLS score). Always set width and height or use CSS aspect-ratio.
Re-compressing already compressed images. Converting a quality-60 JPEG to WebP at quality 80 doesn't improve anything — the quality was already lost in the JPEG stage. Always convert from the highest-quality source available.
Ignoring PNG optimization. A 500KB screenshot as unoptimized 32-bit PNG might be 100KB after pngquant (indexed) or 80KB as lossless WebP. PNG optimization is the most overlooked win.
Image optimization is not a single technique — it's a stack. Format selection saves 25-50%. Responsive sizing saves 30-60% on mobile. Lazy loading reduces initial load by skipping offscreen images. Preloading the LCP image shaves 200-500ms. Combined, these techniques can reduce image-related load time by 60-80%.
Start with the highest-impact step for your site: if you're serving JPEG or PNG everywhere, convert to WebP or AVIF for an immediate 25-50% bandwidth reduction. If you're already on WebP, add responsive sizing via srcset. If you've done both, preload the LCP image and lazy load everything else.
ChangeThisFile handles the format conversion step: JPG to WebP, PNG to WebP, JPG to AVIF, PNG to AVIF — all free, all in your browser.