Skip to main content
vannsl.io | Blog

Modern Image Formats on the Web: WebP and AVIF Without the Headache

Tired of seeing this Google Lighthouse diagnostic?

Lighthouse diagnostic about using next gen image formats

Those fancy modern image formats that supposedly only work in Chrome? Maybe you've considered using progressive enhancement—serving AVIF to capable browsers and falling back to JPEGs for the rest—but it all sounds a bit too complicated?

Let's take a proper look at what's possible in 2025 and how easy it actually is to use modern formats.


🧱 HTML Implementation: The <picture> Element

Let's start with the trusty <picture> tag:

<picture>
  <source type="image/avif" srcset="/image.avif 1000w">
  <source type="image/webp" srcset="/image.webp 1000w">
  <img loading="lazy" decoding="async" src="/image.jpeg" alt="My image" width="1000" height="1000">
</picture>

The <picture> element allows the browser to choose the best image format: AVIF, WebP, or JPEG as a fallback. The loading="lazy" attribute delays loading the image until it's near the viewport, which helps improve page load speed. The decoding="async" attribute decodes the image in the background, allowing the page to render faster without waiting for the image to finish decoding. While the number 1000 here is only an example, explicitly setting width and height helps the browser allocate the correct layout space before the image loads, which improves visual stability and avoids layout shifts—especially important for Core Web Vitals. Additionally, the srcset attribute allows the browser to choose the most appropriate image based on screen size and resolution.

According to w3schools, browser support for <picture> is excellent.


🎨 CSS Option: image-set() for Backgrounds

If you're dealing with background images instead of inline <img> elements, CSS offers a similar mechanism:

.element {
  background-image: image-set(
    url("image.avif") type("image/avif"),
    url("image.webp") type("image/webp"),
    url("image.jpg") type("image/jpeg")
  );
}

This is supported in Safari 17+, as seen on Can I use.

For most use cases, you'll be covered using either <picture> for inline images or image-set() for backgrounds. However, because browser support for <picture> is more consistent, it's usually the safer choice. When possible, avoid using background images for critical content.

In October 2024, I talked about the image-set() CSS functional notation with my Working Draft Co-Host Schepp in the episode Revision 635: State of CSS 2024, Teil 3/3.


🛠️ Converting Images to WebP and AVIF

So how do you actually get your images into WebP or AVIF? While online tools exist, they're often a black box—you don't always know what optimizations or metadata stripping might be happening behind the scenes. Not to mention the privacy aspect of uploading your files.

If you're more comfortable on the command line and prefer open-source tooling, you're in luck.


🎬 Conversion via ffmpeg

On macOS (or any system with ffmpeg installed), you can convert a JPEG to AVIF with:

ffmpeg -i image.jpg -c:v libaom-av1 -crf 30 -b:v 0 image.avif

Let's break down what each part of this command does:

  1. ffmpeg:
    This is the command-line tool for processing video, audio, and images. It supports a variety of formats and codecs, making it an excellent choice for converting media files.
  2. -i image.jpg:
    This specifies the input file. In this case, we’re using image.jpg as the source file. The -i option tells ffmpeg which file to process.
  3. -c:v libaom-av1:
    This option sets the codec to use for the video (or in this case, the image). libaom-av1 is the encoder for the AVIF format, specifically using the AV1 codec, which is known for excellent compression and quality.
  4. -crf 30:
    The Constant Rate Factor (CRF) controls the output quality. Lower CRF values result in better quality (and larger file sizes), while higher CRF values reduce the quality and the file size. In this case, 30 is a reasonable value for a balance between quality and file size. Typically, CRF values range from 0 (best quality) to 51 (worst quality).
  5. -b:v 0:
    This option controls the target bitrate. When set to 0, it tells ffmpeg to use variable bitrate encoding, where the bitrate adjusts based on the complexity of the image to maintain a consistent quality throughout. For AVIF, this is usually set to 0 for the best results.
  6. image.avif:
    This is the output file. ffmpeg will take the input file image.jpg, convert it, and save it as image.avif.

🧪 Conversion via Sharp (Node.js)

Instead of manually converting files every time, consider scripting it—either as a part of your build pipeline or as a simple CLI tool. For this, create a script to convert your images in your project using Sharp, a fast image processing library for Node.js.

I published a repository node-convert-sharp with a basic script. Here's a streamlined version of such a script:

#!/usr/bin/env node

import sharp from 'sharp';
import path from 'path';
import fs from 'fs/promises';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const inputPath = path.join(__dirname, 'input.jpg');
const outputDir = path.join(__dirname, 'output');
await fs.mkdir(outputDir, { recursive: true });

const baseName = path.basename(inputPath, path.extname(inputPath));

// Convert to WebP
try {
  await sharp(inputPath)
    .toFormat('webp')
    .toFile(path.join(outputDir, `${baseName}.webp`));
  console.log('✅ WebP success');
} catch (err) {
  console.error('❌ Error WebP:', err);
}

// Convert to AVIF
try {
  await sharp(inputPath)
    .toFormat('avif')
    .toFile(path.join(outputDir, `${baseName}.avif`));
  console.log('✅ AVIF success');
} catch (err) {
  console.error('❌ Error AVIF:', err);
}

🧰 Running the Script

  1. Install sharp:

    npm install sharp
  2. Ensure your package.json includes:

    {
      "type": "module"
    }
  3. Make the script executable:

    chmod +x convert.js
  4. Run it:

    ./convert.js

Because of the #!/usr/bin/env node shebang at the top, there's no need to call node convert.js explicitly—the OS will handle it for you.


✅ Final Thoughts

Using modern image formats doesn't have to be a headache. With good browser support, native HTML and CSS solutions, and reliable open-source conversion tools, there's little excuse not to start using WebP and AVIF today.

Faster pages, smaller files, happier users.