WebP vs AVIF: How to Choose, with the picture Tag and CDN Conversion
The short version of WebP vs AVIF: AVIF is usually smaller at the same visual quality, especially for photos, but it encodes slowly and can blur text edges in some cases. WebP is smaller than JPEG/PNG, encodes fast, has wider support, and is safer for screenshots and images with transparency. In practice you usually produce both, let the browser pick with <picture>, and keep JPEG/PNG as the fallback.
Picking the right format solves only half the problem. The other half is dimensions and load order: sending a 2400px-wide AVIF to a phone is still wasteful. Below you will find recommendations by image type, HTML templates you can copy, a batch conversion script, and finally the caching traps that show up when a CDN converts formats automatically.
What is the actual difference between WebP and AVIF?
Both formats were designed to replace JPEG and PNG, and both support lossy, lossless, and transparency. The main differences:
| Aspect | WebP | AVIF |
|---|---|---|
| Codec basis | VP8 (lossy) / a dedicated lossless codec | Intra-frame compression from the AV1 video codec |
| Lossy photo size | Usually smaller than JPEG | Usually a bit smaller than WebP again; the gap varies by image |
| Encoding speed | Fast, suitable for processing many files at build time | Much slower; leave time for batch conversion of large images |
| Text and thin lines | Lossless mode works well | Text edges can turn fuzzy at low quality settings; watch the chroma subsampling |
| Bit depth / HDR | 8-bit only | 10/12-bit supported |
| Animation | Supported | Supported, but tooling and compatibility are less mature than WebP |
| Browser support | All major browsers | Recent versions of all major browsers; old Safari and old operating systems are the main gaps |
On compatibility: many comparison articles online were written around 2021, when only Chrome supported AVIF, and their conclusions are out of date. Recent versions of Chrome, Firefox, Edge, and Safari can all decode AVIF. The gaps are mainly old iOS / macOS devices that no longer get updates and some embedded WebViews. Check MDN and caniuse for current version numbers rather than copying them from old posts. For sites serving users in China, the engine inside some in-app browsers (WeChat and similar) can be quite old, which is the main reason to keep WebP and JPEG fallbacks.
Figures like "AVIF is X% smaller than WebP" vary wildly between test sets. This article does not give a universal ratio. Measure with your own images; the method is described below.
Which format should each type of image use?
Choosing by content is more reliable than choosing by "which format is more advanced":
| Image type | First choice | Alternative / fallback | Notes |
|---|---|---|---|
| Photos, banners, product images | Lossy AVIF | Lossy WebP → JPEG | Where AVIF gains the most |
| UI screenshots, text-heavy images | Lossless or high-quality lossy WebP | PNG | For AVIF, use 4:4:4 chroma subsampling and raise the quality, or text edges look soft |
| Illustrations, flat-color graphics | Try both | PNG | With large flat areas, lossless WebP is often already tiny |
| Icons and logos with transparency | SVG | Lossless WebP → PNG | Don't use a bitmap if it can be vector |
| Animations | Video (MP4/WebM) | Animated WebP → GIF | For long animations, switching to <video> saves more |
There is only one criterion: for the same image at the same perceived quality, which file is smaller. Compare by eye at 100% and 200% zoom, paying attention to the three places most likely to break: text edges, gradient skies, and skin tones on faces.
How do you write picture, srcset, and sizes?
This template covers most content images. Copy it and change the paths and widths:
<picture>
<source
type="image/avif"
srcset="/img/hero-640.avif 640w, /img/hero-1024.avif 1024w, /img/hero-1600.avif 1600w"
sizes="(max-width: 768px) 100vw, 768px">
<source
type="image/webp"
srcset="/img/hero-640.webp 640w, /img/hero-1024.webp 1024w, /img/hero-1600.webp 1600w"
sizes="(max-width: 768px) 100vw, 768px">
<img
src="/img/hero-1024.jpg"
srcset="/img/hero-640.jpg 640w, /img/hero-1024.jpg 1024w, /img/hero-1600.jpg 1600w"
sizes="(max-width: 768px) 100vw, 768px"
width="1600" height="900"
alt="Text description of the main page image"
decoding="async">
</picture>
A few things that are easy to get wrong:
- The order of
<source>elements is the priority order. The browser goes top to bottom and takes the firsttypeit supports, so AVIF must come before WebP. sizesmust match the actual CSS width. The example above means: when the viewport is 768px or narrower the image fills the screen width, otherwise it is a fixed 768px. The browser multiplies this value by the device pixel ratio and picks the closest entry insrcset. Ifsizesis wrong, the browser downloads an image that is too large or too small.widthandheightmust be set on the<img>. Use the original image's dimensions; the browser derives the aspect ratio from them and reserves space in advance, so the text doesn't get pushed down when the image finishes loading (CLS, layout shift). Also addimg { max-width: 100%; height: auto; }in your CSS.- Put the attributes on the
<img>.alt,loading,fetchpriority, andwidth/heightonly take effect on<img>;<source>only supplies candidate files.
Where do loading="lazy" and fetchpriority go?
- The largest image above the fold (usually the LCP element): do not add
loading="lazy"; addfetchpriority="high"so the browser requests it early. - Images below the fold: add
loading="lazy"so they load when scrolled near. - The hidden slides in a carousel:
loading="lazy", or insert them only after user interaction.
The most common mistake is a framework or CMS adding loading="lazy" to every image, which also delays the hero image and makes LCP worse. To check, open the DevTools Network panel, sort by the Priority column, and confirm the hero image is High.
How do you choose the size steps?
You don't need a step every 100px. Take the largest CSS width the image is actually displayed at on the page, multiply by 1x and 2x, and add one phone-sized step. For example, if body images display at most 768px wide, steps of 640, 1024, and 1600 work. More steps mean more build output and more objects in the CDN cache, with very little benefit.
How do you batch-convert from the command line?
Squoosh CLI used to be the common recommendation, but its command-line package is no longer maintained. For new projects, use sharp (Node.js, built on libvips) or the official encoders cwebp / avifenc directly.
Generate multiple sizes and formats with sharp
npm i -D sharp
Save the following script as scripts/images.mjs:
import sharp from "sharp";
import { readdir, mkdir } from "node:fs/promises";
import path from "node:path";
const SRC = "assets/images";
const OUT = "public/img";
const WIDTHS = [640, 1024, 1600];
await mkdir(OUT, { recursive: true });
const files = (await readdir(SRC)).filter(f => /\.(jpe?g|png)$/i.test(f));
for (const file of files) {
const name = path.parse(file).name;
const input = sharp(path.join(SRC, file));
const { width: origW } = await input.metadata();
for (const w of WIDTHS) {
if (w > origW) continue; // don't upscale the original
const base = input.clone().resize({ width: w });
await base.clone().avif({ quality: 50, effort: 4 }).toFile(`${OUT}/${name}-${w}.avif`);
await base.clone().webp({ quality: 75 }).toFile(`${OUT}/${name}-${w}.webp`);
await base.clone().jpeg({ quality: 78, mozjpeg: true }).toFile(`${OUT}/${name}-${w}.jpg`);
}
console.log("done", file);
}
node scripts/images.mjs
The quality values are a starting point, not a standard answer. For screenshots, change to .webp({ lossless: true }), or add chromaSubsampling: "4:4:4" to AVIF and raise quality. A higher effort compresses smaller but runs slower; lower it if your CI is short on time.
Process a single file with cwebp / avifenc
cwebp -q 75 photo.jpg -o photo.webp
cwebp -lossless screenshot.png -o screenshot.webp
avifenc -q 60 -s 6 photo.jpg photo.avif
avifenc options differ slightly between versions; go by the output of avifenc --help.
How do you verify the files actually got smaller?
ls -l public/img | sort -k5 -n | awk '{print $5, $9}'
Compare the .avif, .webp, and .jpg of the same size step side by side. If an image's AVIF ends up larger than its WebP (occasionally the case for small or flat-color images), you can drop the AVIF source for that image in <picture>, or add a rule to the script that deletes the AVIF when it is larger than the WebP.
Why does caching go wrong when a CDN converts formats automatically?
Many CDNs and image services support "one URL, different formats by browser capability": they read the request's Accept header and return AVIF if it contains image/avif, WebP if it contains only image/webp, and the original otherwise. You don't have to change the HTML, but there are three caching traps.
Trap 1: Without Vary, the cache serves the wrong format to other users
When one URL maps to several different bodies, the response must carry Vary: Accept to tell every intermediate cache that the response depends on the Accept header. If the origin or image service negotiates the format but omits Vary, a shared cache layer (CDN, corporate proxy, reverse proxy) may store the AVIF version first and later send it to an older browser that can't decode AVIF, which shows up as broken images.
Trap 2: With Vary: Accept, the hit rate drops
The reverse problem: if the CDN faithfully keys the cache on the full raw Accept string, you get a different mess. Different browsers and versions send all kinds of Accept values, and each variant takes its own cache entry. The same image is stored many times at the edge, the hit rate falls, and origin fetches increase.
The right approach is to have the CDN normalize Accept into a few buckets (for example avif / webp / original format) before putting it in the cache key. How to configure this depends on your CDN: some products handle it automatically when image conversion is enabled, some need an extra rule in the cache key settings, and some have limited support for any Vary other than Accept-Encoding and may simply not cache. Before you start, read the "cache key" and "Vary" sections of your CDN's official documentation.
Trap 3: Browser cache and CDN cache disagree
If the image URL contains a content hash, you can give it a long cache lifetime: Cache-Control: public, max-age=31536000, immutable. But format-negotiated URLs usually have no hash. After the original image is updated, the cached copies in every format need to be purged together; purging only the original leaves you with "Chrome sees the new image, older browsers see the old one." For a full guide to cache headers, see How to Configure Cache-Control Headers.
Check with curl that the CDN caches each format correctly
URL=https://example.com/img/hero.jpg
# Simulate a browser that supports AVIF
curl -sI -H "Accept: image/avif,image/webp,*/*" $URL | grep -iE "content-type|vary|cache|age"
# Simulate a browser that supports only WebP
curl -sI -H "Accept: image/webp,*/*" $URL | grep -iE "content-type|vary|cache|age"
# Simulate a client that supports neither
curl -sI -H "Accept: */*" $URL | grep -iE "content-type|vary|cache|age"
What to check:
- The three requests return
Content-Typeofimage/avif,image/webp, and the original format respectively. - The response carries
Vary: Accept, or the CDN documentation clearly states that the format is already part of the cache key. - Run each command twice in a row; on the second run the cache status header (the name differs by CDN, e.g.
cf-cache-status,x-cache) shows a hit. - The third command does not return AVIF/WebP. If it does, negotiation or the cache key is broken, and older browsers will get broken images.
If you'd rather not take on these risks, generate the multi-format files at build time and reference them explicitly with <picture>. URLs and formats then map one to one, which is the simplest thing to cache. CDN auto-conversion suits sites with a large volume of images, user-uploaded images, or where changing the HTML is expensive.
Beyond image optimization, what else should you look at?
If the page is still slow after the images are small, the problem is usually the network path: CDN nodes far from the user, slow origin fetches, or third-party resources blocking rendering. For access from mainland China, see:
- For sites hosted on overseas static platforms, work through the layers with How to Diagnose a Static Site That Is Slow in China.
- Fonts are also a big part of the first screen; see Self-Hosting Google Fonts: Subsetting and font-display.
- For ideas on using Cloudflare for caching and origin protection, see Speeding Up AI Apps with Cloudflare; its cache rules section applies to images too.
When choosing a public frontend CDN, first use the cdnping online speed test to see the connection and round-trip times from your network to each public CDN, then compare with the CDN speed ranking and How to Choose a Public CDN for China. If you want to put your own site on an edge network, cdnping Edge has the service details. Note that acceleration inside mainland China requires an ICP filing (China's website registration requirement) and must go through a licensed provider.
Pre-launch checklist
- Photos have AVIF, WebP, and JPEG variants; screenshots have at least WebP and PNG.
-
<source>order is AVIF first, WebP second. -
sizesmatches the actual displayed width in CSS. - Every
<img>haswidth,height, andalt. - The hero image has no
loading="lazy"and hasfetchpriority="high". - All images below the fold have
loading="lazy". - Spot-check a few images to confirm AVIF is not larger than WebP and text edges are clear.
- If you use CDN auto-conversion, the curl checks with all three
Acceptvalues pass. - Hashed image URLs use a long cache lifetime with
immutable.
When you're done, filter by Img in the DevTools Network panel to see the formats and sizes actually downloaded, then use Lighthouse to see whether LCP and CLS changed. Treat your own site's measurements as the final numbers.