Modern image formats save 30-70% of bytes vs JPEG/PNG at the same visual quality. WebP is universally supported and saves ~30%. AVIF is newer with 95%+ browser support and saves up to 70%. Used together with proper fallback, you ship the smallest format every browser supports. This guide covers picture element fallback, encoding pipelines, server-side content negotiation, and the measurement to verify the wins. For related fixes, see the Image Optimisation Fixes index.
| Format | Compression vs JPEG | Browser support | Best for |
|---|---|---|---|
| JPEG | baseline | universal | photographs, fallback |
| PNG | often LARGER | universal | graphics, transparency, fallback |
| WebP | ~30% smaller | ~98% (Chrome, Firefox, Safari 14+, Edge) | general purpose, primary modern format |
| AVIF | ~50-70% smaller | ~95% (Chrome 85+, Firefox 93+, Safari 16.4+) | large hero images, photos, max savings |
| SVG | N/A (vector) | universal | logos, icons, line art |
<picture>
<source type="image/avif" srcset="/img/hero.avif">
<source type="image/webp" srcset="/img/hero.webp">
<img src="/img/hero.jpg"
alt="Description"
width="1200" height="675"
loading="lazy">
</picture>
Browser tries AVIF first. If it doesn't support AVIF (older Safari, older Firefox), it tries WebP. If WebP fails too, it falls back to the img element's JPEG src.
<picture>
<source type="image/avif"
srcset="/img/hero-400.avif 400w,
/img/hero-800.avif 800w,
/img/hero-1200.avif 1200w,
/img/hero-2400.avif 2400w"
sizes="(max-width: 800px) 100vw, 800px">
<source type="image/webp"
srcset="/img/hero-400.webp 400w,
/img/hero-800.webp 800w,
/img/hero-1200.webp 1200w,
/img/hero-2400.webp 2400w"
sizes="(max-width: 800px) 100vw, 800px">
<img src="/img/hero-1200.jpg"
srcset="/img/hero-400.jpg 400w,
/img/hero-800.jpg 800w,
/img/hero-1200.jpg 1200w,
/img/hero-2400.jpg 2400w"
sizes="(max-width: 800px) 100vw, 800px"
alt="Description"
width="1200" height="675"
loading="lazy">
</picture>
Each format gets its own srcset with multiple sizes. Browser picks the smallest format AND the right size for the viewport. Most efficient approach.
const sharp = require('sharp');
async function convertImage(input) {
await sharp(input)
.webp({ quality: 80 })
.toFile(input.replace(/\.(jpg|png)$/, '.webp'));
await sharp(input)
.avif({ quality: 60 })
.toFile(input.replace(/\.(jpg|png)$/, '.avif'));
}
npm install -g @squoosh/cli
squoosh-cli --webp '{"quality": 80}' \
--avif '{"cqLevel": 33}' \
./images/*.jpg
// next.config.js — automatic AVIF + WebP
module.exports = {
images: {
formats: ['image/avif', 'image/webp']
}
};
// Component handles negotiation automatically
import Image from 'next/image';
<Image src="/hero.jpg" alt="..." width={1200} height={675} />
Image-CDN services convert on the fly based on browser Accept header. Add the service URL prefix, modern formats served automatically:
<img src="https://yoursite-cdn.com/auto/hero.jpg" alt="..."> <!-- CDN reads request Accept: image/avif and returns AVIF -->
Alternative to picture element: nginx or Apache reads the Accept header and rewrites the URL to serve the modern format directly.
# nginx — rewrite to .webp if browser supports it
location ~* \.(jpg|jpeg|png)$ {
if ($http_accept ~* "webp") {
set $webp_uri $uri.webp;
}
try_files $webp_uri $uri =404;
}
Trade-off: no AVIF/WebP fallback chain in browsers that support both. Less granular control. Picture element is more declarative; server negotiation is less work in templates.
Don't use 100% quality. Modern format savings come from accepting slightly less quality at much smaller size. Quality 75-85 is visually indistinguishable from quality 100 for most photos.
| Format | Recommended quality | Notes |
|---|---|---|
| JPEG | 80-85 | quality 90+ rarely worth the size |
| WebP | 75-85 | visually similar to JPEG 80-85 at smaller size |
| AVIF | 50-70 (cqLevel 30-40) | aggressive compression, still excellent quality |
| PNG | N/A (lossless) | use pngquant for lossy PNG-8 alternative |
Vector graphics scale infinitely, work at any DPI, often smaller than raster. Use SVG for:
Optimise SVG with SVGO to remove editor metadata, unused defs, redundant nodes:
npm install -g svgo svgo input.svg -o output.svg
# Total image bytes on a page, before
curl -s https://yoursite.com/page | \
grep -oE 'src="[^"]+\.(jpg|png)"' | \
awk -F'"' '{print $2}' | \
xargs -I{} curl -sI https://yoursite.com{} | \
grep -i content-length
Sum the byte counts. Compare to AVIF-served totals after deployment.
Verify format upgrades and measure byte savings.
Run Image Audit →