Automate Image Optimization: WebP/AVIF Pipelines That Speed Up Sites
Automate image conversion and compression into WebP and AVIF formats to improve Core Web Vitals and boost SEO performance.
Why Image Optimization Decides Performance
Images make up the majority of page weight on most sites — hybrid, static, dynamic, whatever. According to HTTP Archive data, images often account for over 50% of transferred bytes in any given transmission. If those assets are bloated (which they almost always are) then no amount of script minification will save you. This is why modern formats like WebP and AVIF were developed in the first place mdash; they achieve smaller file sizes at equal or better quality than JPEG or PNG, directly improving Core Web Vitals scores.
Instituting automated pipelines mean you never have to rely on designers to export “the right way” or manually compress every upload like it is 2004. The system handles conversion, compression, and delivery without constant upkeep. The payoff of this effort is faster load times, stronger Lighthouse scores, and better SEO visibility.
WebP vs. AVIF: Choosing the Right Format
While it is true that both WebP and AVIF formats deliver next-generation compression, it is important to make distinctions about the different needs that they serve. WebP is widely supported, balances quality and speed, and works well for most media applications in modern websites. AVIF pushes compression even further than WebP does, to the point of often cutting file size by 20–30% more and therefore diminishing quality and slowing encoding. Google’s own WebP documentation and the AVIF spec confirm both formats’ value.
My default response to this issue is simple, perhaps overly so — generate both. Serve AVIF to browsers that you know support it and keep WebP as the fallback when AVIF is not supported. A single pipeline can produce both automatically, using libraries like sharp or imagemin. For a deeper dive into strategy, see our guide on Optimizing Images for Performance.
Designing an Automated Pipeline
A good image pipeline runs on every asset, every deploy — no doubts. The sequence is simple: intake raw files, convert to WebP and AVIF, compress, resize for breakpoints, and output to a CDN-backed folder. Automating this once saves dozens if not hundreds of hours cumulatively as well as eliminates human error from the whole process; it is extremely easy to forget an asset and pay for it in production later. I take the same approach I covered in Checklist Before Launching a Site—repeatable systems reduce mistakes and make quality scalable.
This process pairs naturally with component-driven design (modern web design, if we are being honest). When you use Component Thinking for Static Sites, each image call becomes predictable and makes it easier to drop in responsive <picture> elements with proper fallbacks effortlessly.
Sample Workflow
- Ingest: drop raw images into
/src/assets/raw/. - Transform: run
sharpto generate WebP and AVIF with quality presets. - Resize: produce multiple widths (e.g., 400px, 800px, 1200px).
- Compress: apply lossless or near-lossless compression.
- Deploy: push to CDN with long-cache headers.
Drop-in Optimizer (macOS/Linux): AVIF + WebP (+ JPEG Fallback)
Convert anything you place in /src/assets/raw/ into /src/assets/optimized/ as AVIF, WebP, and a progressive JPEG fallback at multiple widths. Safe defaults, subfolder-aware, and easy to wire into any static build.
Install (once)
npm i sharp
Assumed project layout
src/
assets/
raw/
optimized/
scripts/
optimize-images.mjs
Script: scripts/optimize-images.mjs
/* eslint-disable no-console */
import fs from "node:fs/promises";
import path from "node:path";
import url from "node:url";
import sharp from "sharp";
/**
* Image optimizer (macOS/Linux)
* - Reads: src/assets/raw
* - Writes: src/assets/optimized
* - Outputs: AVIF, WebP, JPEG fallback at widths [400, 800, 1200, 1600]
* - Safe defaults; preserves subfolders; progressive JPEG
*/
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const RAW_DIR = process.env.RAW_DIR || path.join(__dirname, "..", "src", "assets", "raw");
const OUT_DIR = process.env.OUT_DIR || path.join(__dirname, "..", "src", "assets", "optimized");
// Tweak to taste
const WIDTHS = [400, 800, 1200, 1600];
const AVIF_QUALITY = 45; // great size/quality balance
const WEBP_QUALITY = 75;
const JPEG_QUALITY = 82;
const SUPPORTED_EXTS = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".tif", ".tiff", ".svg"]);
async function* walk(dir) {
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walk(full);
} else {
yield full;
}
}
}
async function ensureDir(p) {
await fs.mkdir(p, { recursive: true });
}
function outPathFor(inputFile, width, ext) {
// Preserve folder structure under /optimized
const rel = path.relative(RAW_DIR, inputFile);
const { dir, name } = path.parse(rel);
return path.join(OUT_DIR, dir, `${name}@${width}${ext}`);
}
async function processOne(file) {
const { ext } = path.parse(file);
if (!SUPPORTED_EXTS.has(ext.toLowerCase())) return;
// sharp can rasterize vector formats (e.g., SVG) at requested widths
const src = sharp(file, { failOnError: false });
for (const width of WIDTHS) {
const base = src.clone().resize({ width, withoutEnlargement: true });
// AVIF
{
const out = outPathFor(file, width, ".avif");
await ensureDir(path.dirname(out));
await base.clone().avif({ quality: AVIF_QUALITY, effort: 4 }).toFile(out);
}
// WebP
{
const out = outPathFor(file, width, ".webp");
await ensureDir(path.dirname(out));
await base.clone().webp({ quality: WEBP_QUALITY }).toFile(out);
}
// JPEG fallback (progressive)
{
const out = outPathFor(file, width, ".jpg");
await ensureDir(path.dirname(out));
await base.clone().jpeg({ quality: JPEG_QUALITY, progressive: true, mozjpeg: true }).toFile(out);
}
}
}
async function main() {
await ensureDir(OUT_DIR);
let filesProcessed = 0;
for await (const file of walk(RAW_DIR)) {
try {
await processOne(file);
filesProcessed++;
} catch (err) {
console.error("Failed:", file, err?.message || err);
}
}
console.log(`✅ Optimized ${filesProcessed} file(s).`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
Use in HTML
Example responsive usage with AVIF & WebP sources and lazy loading:
<picture>
<source type="image/avif" srcset="
/src/assets/optimized/portfolio/hero@400.avif 400w,
/src/assets/optimized/portfolio/hero@800.avif 800w,
/src/assets/optimized/portfolio/hero@1200.avif 1200w,
/src/assets/optimized/portfolio/hero@1600.avif 1600w" sizes="(min-width: 1024px) 1200px, 90vw">
<source type="image/webp" srcset="
/src/assets/optimized/portfolio/hero@400.webp 400w,
/src/assets/optimized/portfolio/hero@800.webp 800w,
/src/assets/optimized/portfolio/hero@1200.webp 1200w,
/src/assets/optimized/portfolio/hero@1600.webp 1600w" sizes="(min-width: 1024px) 1200px, 90vw">
<img
src="/src/assets/optimized/portfolio/hero@800.jpg"
srcset="
/src/assets/optimized/portfolio/hero@400.jpg 400w,
/src/assets/optimized/portfolio/hero@800.jpg 800w,
/src/assets/optimized/portfolio/hero@1200.jpg 1200w,
/src/assets/optimized/portfolio/hero@1600.jpg 1600w"
sizes="(min-width: 1024px) 1200px, 90vw"
alt="Project hero image showing the final UI on a laptop and phone"
width="1200" height="800" loading="lazy" decoding="async" fetchpriority="low" class="rounded-lg shadow" />
</picture>
Make It Part of the Build (Generic SSG + Eleventy)
Generic SSG (any framework)
Add a prebuild step so images are optimized before your site compiles:
{
"scripts": {
"prebuild": "node scripts/optimize-images.mjs",
"build": "YOUR_SSG_BUILD_COMMAND",
"dev": "YOUR_DEV_COMMAND"
}
}
If your SSG expects assets under a different output path, either change OUT_DIR in the script or copy from src/assets/optimized to your final dist assets directory during build.
Eleventy (11ty)
Watch the raw folder, run the optimizer before build, and passthrough the optimized files:
// package.json
{
"scripts": {
"prebuild": "node scripts/optimize-images.mjs",
"build": "ELEVENTY_ENV=production npx @11ty/eleventy",
"dev": "npx @11ty/eleventy --serve"
}
}
// .eleventy.js
module.exports = function(eleventyConfig) {
// Re-run when raw images change
eleventyConfig.addWatchTarget("src/assets/raw");
// Passthrough optimized outputs to the site
eleventyConfig.addPassthroughCopy({ "src/assets/optimized": "assets/optimized" });
return {
dir: {
input: "src",
output: "_site"
}
};
};
Now npm run build optimizes images, compiles the site, and ships _site/assets/optimized. In templates, reference the optimized paths (as in the <picture> example above).
SEO and Conversion Benefits
Google has stated in its helpful content guidance that performance is a ranking signal which only grows more prominent with time. Optimized images improve core web vitals, which then directly boost SEO — in a hyper-competitive market the difference between success and mediocrity can be as small as speed. But beyond rankings, faster visuals build trust. I wrote about this dynamic in Building Trust Through Brand Consistency—when a site feels polished and immediate, visitors attribute professionalism to the brand.
There’s also a direct conversion lift when technical aspects of a site are optimized because faster-loading product images, portfolios, and case studies reduce user abandonment rates. That’s why we run image audits alongside site speed assessments. Every second saved compounds across hundreds of sessions and illustrates to users and search engines professionalism.
Security, Governance, and Reliability
Image pipelines touch infrastructure in a more obvious way than most developers consider — misconfigured headers can expose vulnerabilities or block caching in a single push. Use the same discipline described in All About Headers: apply Cache-Control, Content-Type, and Referrer-Policy correctly.
Governance in this context means logging every transformation from the last build so debugging is easy — this includes storing the hashes of original and output files so you can prove fidelity. This is particularly important if your site hosts user-generated content because demonstrable build resiliency in the system echoes the principles from Progressive Enhancement in Practice. Redundancy in media assets allows a smooth user experience in all conditions, like if an AVIF fails to load, a WebP or JPEG should still appear instantly.
Measurement and Continuous Improvement
A lot of developers will have the grit to set this system up, but their endurance falls short of implementing any formal measurements. Use tools like PageSpeed Insights and WebPageTest to benchmark improvements and monitor bandwidth savings and CDN cache hit ratios — compare before-and-after Lighthouse scores. This is the same observability mindset I recommend in performance testing tools comparison—if you can’t see it, you can’t optimize it.
Measurement data allows developers to feed results back into the pipeline, as in, adjust quality presets if artifacts appear or tune breakpoints based on analytics. Professionalized optimization is not a one-time act but instead an ongoing feedback loop.
Automate Once, Benefit Forever
Automated image optimization is one of the highest-ROI improvements you can make to a site, especially one that is content focused, as most service businesses are. By generating WebP and AVIF consistently, securing delivery with headers, and measuring outcomes, you lock in faster pages and stronger SEO. Unlike ad spend or campaigns, these benefits compound over time and will not simply end because the advertisement money ran out.
Performance is strategy. Automation simply enforces it. When every deploy bakes optimization in, you stop relying on luck or memory. The result is a faster, safer, and more professional experience—one that both search engines and humans reward.