WebP vs AVIF: How to Choose, with the picture Tag and CDN Conversion

Blog / Performance

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:

AspectWebPAVIF
Codec basisVP8 (lossy) / a dedicated lossless codecIntra-frame compression from the AV1 video codec
Lossy photo sizeUsually smaller than JPEGUsually a bit smaller than WebP again; the gap varies by image
Encoding speedFast, suitable for processing many files at build timeMuch slower; leave time for batch conversion of large images
Text and thin linesLossless mode works wellText edges can turn fuzzy at low quality settings; watch the chroma subsampling
Bit depth / HDR8-bit only10/12-bit supported
AnimationSupportedSupported, but tooling and compatibility are less mature than WebP
Browser supportAll major browsersRecent 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 typeFirst choiceAlternative / fallbackNotes
Photos, banners, product imagesLossy AVIFLossy WebP → JPEGWhere AVIF gains the most
UI screenshots, text-heavy imagesLossless or high-quality lossy WebPPNGFor AVIF, use 4:4:4 chroma subsampling and raise the quality, or text edges look soft
Illustrations, flat-color graphicsTry bothPNGWith large flat areas, lossless WebP is often already tiny
Icons and logos with transparencySVGLossless WebP → PNGDon't use a bitmap if it can be vector
AnimationsVideo (MP4/WebM)Animated WebP → GIFFor 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:

  1. The order of <source> elements is the priority order. The browser goes top to bottom and takes the first type it supports, so AVIF must come before WebP.
  2. sizes must 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 in srcset. If sizes is wrong, the browser downloads an image that is too large or too small.
  3. width and height must 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 add img { max-width: 100%; height: auto; } in your CSS.
  4. Put the attributes on the <img>. alt, loading, fetchpriority, and width/height only 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"; add fetchpriority="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-Type of image/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:

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.
  • sizes matches the actual displayed width in CSS.
  • Every <img> has width, height, and alt.
  • The hero image has no loading="lazy" and has fetchpriority="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 Accept values 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.

Related articles

Keep reading