A WebP Build Pipeline for Static Sites That You Will Not Regret
Every static site generator has an image plugin, and they all work. The differences that matter show up later — when the build takes eleven minutes, when nobody can remember why a particular image looks soft, or when you want to switch formats and discover the originals are gone.
Decision 1: one master per image, everything else generated
Keep a single high-quality source per image in version control or object storage, and treat every WebP, AVIF and resized variant as a build artefact that can be deleted and regenerated at any moment.
This sounds obvious and is violated constantly. The failure mode is that somebody converts a folder to WebP, deletes the originals to save space, and eighteen months later you cannot generate AVIF, cannot produce a print-quality export, and cannot re-encode at a different quality without compounding loss.
Decision 2: cache aggressively, keyed on content
Image processing dominates build time on any content-heavy site. Cache generated variants keyed on a hash of the source file plus the transformation parameters. Then a build that changes one blog post does not re-encode four hundred images.
Commit the cache or persist it in CI. A cold cache on every CI run turns a thirty-second build into a ten-minute one, and that is the thing that eventually makes a team disable image optimisation entirely.
Decision 3: pick your widths deliberately
Three or four widths cover almost everything: roughly 400, 800, 1600, and optionally 2400 for full-bleed hero images. Generating eight or ten sizes multiplies build time, storage and CDN cache fragmentation for a benefit that is close to zero, because the browser picks the smallest candidate at least as large as it needs rather than interpolating.
Decision 4: quality by content type, not one global number
A single quality setting for the whole site is a compromise that serves nothing well. Photographs want lossy at 80-85. Logos, icons, diagrams and screenshots of text want lossless — lossy compression puts visible fuzz around hard edges, and on flat-colour graphics lossless is often smaller anyway.
The simplest workable rule is a directory convention: /images/photos/ gets lossy, /images/ui/ gets lossless. It requires no metadata and no per-image configuration, and it is obvious to the next person.
Decision 5: never upscale
If a source image is 900 pixels wide, do not generate a 1600px variant from it. You will produce a larger file containing no additional information, and it will look softer than the original. Configure the pipeline to skip variants wider than the source — most plugins support this and most people leave it off.
Decision 6: emit picture elements, not bare img tags
Have the pipeline output a full <picture> with AVIF, WebP and a JPG fallback, plus width and height attributes from the source dimensions. Those two attributes prevent layout shift and cost nothing. This is also where loading="lazy" belongs — with an explicit opt-out for the LCP image, which should stay eager and carry fetchpriority="high".
Decision 7: make failures loud
Image pipelines fail silently by default: a corrupt source, an unsupported colour profile, an animated GIF where a still was expected. Configure the build to fail on image processing errors rather than skipping and continuing. A build that goes red is annoying; a site that quietly ships broken images for a week is worse.
What good looks like
A warm-cache build that adds a couple of seconds regardless of library size, three or four widths in three formats per image, quality chosen by content type, correct dimensions in the markup, and a set of source masters you could regenerate everything from tomorrow if a better format appeared. Which, given AVIF and whatever follows it, is not a hypothetical.