Tutorial · images · Published 2026-08-16 · 3 min read

WebP and AVIF conversion inside your CI pipeline

WebP and AVIF conversion in CI: a build-time step with sharp, deterministic output, and a check that no bitmap ships unoptimised.

Optimisation belongs in the build, not the folder

Leaving image conversion to whoever uploads a file produces a mixed folder of JPEGs, PNGs, and PNG-exports that quietly degrades the site. The AVIF pipeline article is the manual version of this idea; this page turns it into a checked build step that runs on every change, so the format set is a property of the repo, not of a person's memory.

In a CI pipeline, the same conversion that built the site can emit the optimised formats and refuse to pass if it finds an unoptimised bitmap, which keeps the webp and avif formats in force by construction.

A deterministic conversion step

The cleanest route inside a Node or static build is sharp, which already powers many static generators and writes both WebP and AVIF from the same code:

import sharp from 'sharp';
const out = [
  sharp('src/hero.jpg').webp({ quality: 80 }).toFile('dist/hero.webp'),
  sharp('src/hero.jpg').avif({ quality: 60, effort: 4 }).toFile('dist/hero.avif'),
];

effort controls encode time; keep it low enough for fast builds on photographic content and higher only where bytes matter and the queue tolerates it. Commit the quality and effort values as named constants so PRs change them deliberately rather than by accident.

Enforce it with a check

The step only earns its keep if it fails the build when skipped. Add a lightweight check that runs after conversion and compares what should exist:

  1. Verify every source image under a known set of directories produced a sibling WebP and AVIF.
  2. Flag any .jpg, .jpeg, or .png that reaches the output directory directly, because the fallback img in markup usually points back at a converted WebP or AVIF, and a raw bitmap that slipped through is a size regression.
  3. Optionally assert a size budget: warn when a converted file exceeds a threshold, since a wrongly-configured encoder can output a near-lossless WebP heavier than the source.

A minimal check could walk dist/assets and fail if it finds an unconverted .png:

const bitmap = await glob('dist/**/*.png');
if (bitmap.length) throw new Error(`Unconverted PNGs: ${bitmap}`);

Keep the check intentionally narrow. Enforcing strict conversion everywhere is brittle, so scope the hard gate to your own asset directories and allow third-party or user-uploaded bits to bypass it, then audit the page to confirm the bytes actually dropped.

The workflow that survives PR review

A repeatable pattern that teams stick with:

  1. Add the conversion to the existing build step, not a second job, so it cannot be forgotten.
  2. Refer converted formats in markup with a fallback, using <picture> as the AVIF pipeline article shows, and keep width and height so layout reserves space.
  3. Gate on the check, so a merge that ships a raw bitmap fails loudly rather than silently slowing the site.
  4. Record the format and byte delta in the PR, so reviewers see the optimisation as a concrete number, the same discipline as the image CLI tools but automated.

CI conversion turns "remember to optimise images" into "images are optimised or the build fails". The format choice, the encoder settings, and a checked build step together make the fast default the only default, which is what keeps a growing site fast.

Need a website built, fixed, optimised, migrated or replaced?

This technical resource is written by CSMBAC, a small design and development studio. If you would rather hand the problem to a professional, the website service page explains how we build enquiry-ready websites.

Explore website services