Master image optimization through CI/CD automation. Learn how to generate AVIF/WebP formats and responsive srcset attributes to crush LCP and improve performance.
We’ve all been there: staring at a Lighthouse report that screams about "properly sized images" while our hero section weighs in at a bloated 1.2MB. I spent three days manually resizing assets for a client project before realizing that if I was doing it, my pipeline was failing me. Manual optimization is a race you cannot win.
If you’re serious about Core Web Vitals, you need a strategy that treats images like code: versioned, automated, and tested.
The goal here is simple: every image added to the repository should automatically spawn a set of variants (AVIF, WebP, and JPEG) at various widths. By leveraging srcset and the <picture> tag, we let the browser decide which file to download based on the user's viewport.
We first tried using a runtime CDN-based transformation service. It was convenient, but the latency hit on the first request for a non-cached asset was inconsistent. We switched to a build-time CI/CD automation approach using sharp and a custom Node.js script. This ensures that by the time the code hits production, the assets are already optimized and ready to serve.
I use a simple GitHub Action that triggers on push to the assets/ directory. It runs a script using sharp (version 0.32.0 or higher) to process the images.
Here is the basic logic I use to generate the variants:
JAVASCRIPTconst sharp = require(CE9178">'sharp'); const widths = [480, 800, 1200]; async function processImage(inputPath) { const pipeline = sharp(inputPath); for (const width of widths) { await pipeline .clone() .resize(width) .webp({ quality: 80 }) .toFile(CE9178">`dist/img/w-${width}.webp`); await pipeline .clone() .resize(width) .avif({ quality: 65 }) .toFile(CE9178">`dist/img/w-${width}.avif`); } }
This script generates roughly 6 files for every 1 source image. It’s not elegant, but it’s fast. In my last build, it reduced our asset directory size by about 65% while maintaining visual fidelity that was indistinguishable to the human eye.
Generating the files is only half the battle. If you don't wire them up to the browser, you’re just wasting storage. You need to leverage srcset to signal the browser about available resolutions.
When you pair this with proper LCP optimization, you ensure the browser picks the smallest viable image for the user's screen. If you're on a mobile device, there is zero reason to download a 1200px wide hero image.
HTMLstyle="color:#808080"><style="color:#4EC9B0">picture> style="color:#808080"><style="color:#4EC9B0">source type="image/avif" srcset="w-480.avif 480w, w-800.avif 800w, w-1200.avif 1200w"> style="color:#808080"><style="color:#4EC9B0">source type="image/webp" srcset="w-480.webp 480w, w-800.webp 800w, w-1200.webp 1200w"> style="color:#808080"><style="color:#4EC9B0">img src="w-800.webp" alt="Hero image" width="800" height="400" fetchpriority="high"> style="color:#808080"></style="color:#4EC9B0">picture>
By adding fetchpriority="high" to your LCP image, you help the browser understand exactly what to prioritize during the initial paint. I’ve seen this combination shave around 300ms off the LCP metric on mid-tier mobile devices. Don't forget to also consider your browser resource prioritization when you have multiple heavy assets competing for bandwidth.
Is this perfect? No. The main drawback is build time. If you have thousands of images, your CI pipeline will crawl. I had to implement a content-hash check to ensure we only re-process images that have actually changed since the last commit.
Also, AVIF encoding is CPU-intensive. If your CI runner is underpowered, you might hit execution time limits. I recommend using a dedicated machine or a long-running runner if your asset library is massive.
Q: Why not just use a service like Cloudinary or Imgix? A: Those services are great, but they introduce a dependency on an external provider and can get expensive at scale. A build-time pipeline gives you full control and zero runtime costs.
Q: Does this break my local development experience?
A: It shouldn't. You can point your local environment to the dist/ folder where your optimized assets live. Just ensure your build script runs locally before you start your dev server.
Q: How do I handle art direction (different crops for mobile vs desktop)?
A: The <picture> element supports the media attribute. You can use it to swap completely different files for different breakpoints if you need more control than simple resizing allows.
The key to image optimization isn't finding the "perfect" compression algorithm; it’s about having a repeatable process that prevents bloat from reaching production in the first place. I’m still experimenting with caching strategies for these generated assets, but moving the heavy lifting to the CI/CD pipeline has been a massive win for our team's workflow. Start small, automate the formats, and watch your LCP metrics trend in the right direction.
Forced synchronous layout is a silent INP killer. Learn how to debug the browser rendering pipeline, avoid layout thrashing, and keep your UI responsive.
Read moreMaster browser resource prioritization using Fetch Metadata and Document Policy. Stop network congestion and improve your LCP by controlling request scheduling.