Skip to content

Image Optimization Pipelines -- WebP, AVIF & Responsive Images

DodaTech Updated 2026-06-22 9 min read

In this tutorial, you'll learn about Image Optimization Pipelines. We cover key concepts, practical examples, and best practices.

Image optimization is the process of reducing image file sizes without visible quality loss, converting to modern formats like WebP and AVIF, and serving responsive sizes to deliver the smallest possible image for each device and viewport.

What You'll Learn

Why It Matters

Images account for 60-80% of a typical web page's total weight. An unoptimized hero image can be 2-5 MB, taking seconds to load on mobile networks. Optimizing images can reduce page weight by 80-90%, improving Lighthouse scores, Core Web Vitals (LCP, CLS), and conversion rates. At DodaTech, our image optimization pipeline converts every tutorial screenshot to WebP with responsive sizes, reducing average image weight from 400 KB to 45 KB -- a 90% reduction without perceptible quality loss.

Real-World Use

An e-commerce site with 10,000 product images saves 15 GB of bandwidth per day by serving WebP instead of JPEG. A news site improves its LCP (Largest Contentful Paint) from 4.2s to 1.8s by implementing responsive images with AVIF. A documentation site reduces its total page weight from 12 MB to 1.5 MB with a build-time image pipeline.

Image Optimization Pipeline

flowchart LR
  A[Source Images PNG/JPEG] --> B[Build Pipeline]
  B --> C[Convert to WebP]
  B --> D[Convert to AVIF]
  B --> E[Resize to Breakpoints]
  E --> F[480px]
  E --> G[768px]
  E --> H[1200px]
  C --> I[Optimized Output]
  D --> I
  F --> I
  G --> I
  H --> I
  I --> J[CDN Cache]
  J --> K[Browser with ]
  style B fill:#f90,color:#fff

Image Format Comparison

Format Compression Browser Support Transparency Animation Best For
WebP Lossy and lossless 96%+ (Chrome, Firefox, Edge, Safari 14+) Yes Yes General web images
AVIF Lossy and lossless 80%+ (Chrome, Firefox, Safari 16.4+) Yes Yes Highest compression
JPEG Lossy 100% No No Photographs, legacy support
PNG Lossless 100% Yes No Screenshots, diagrams
GIF Lossless (256 colors) 100% Yes Yes Simple animations
SVG Vector (scalable) 100% Yes Yes Icons, logos, illustrations

WebP typically achieves 25-35% smaller files than JPEG at equivalent quality. AVIF achieves 50-60% smaller files than JPEG, making it the current leader in compression efficiency.

Build-Time Image Pipeline with Hugo

Hugo Pipes can process images at build time, generating multiple formats and sizes from a single source file.

Hugo Image Processing

{{/* layouts/_default/_markup/render-image.html */}}
{{ $image := .Page.Resources.GetMatch (printf "**%s" .Destination) }}
{{ if $image }}
  {{ $webpSmall := $image.Resize "480x webp q80" }}
  {{ $webpMedium := $image.Resize "768x webp q80" }}
  {{ $webpLarge := $image.Resize "1200x webp q80" }}
  {{ $avifSmall := $image.Resize "480x webp q70" }}
  {{ $avifMedium := $image.Resize "768x webp q70" }}
  {{ $avifLarge := $image.Resize "1200x webp q70" }}
  <picture>
    <source srcset="{{ $avifSmall.RelPermalink }} 480w,
                    {{ $avifMedium.RelPermalink }} 768w,
                    {{ $avifLarge.RelPermalink }} 1200w"
            sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
            type="image/avif">
    <source srcset="{{ $webpSmall.RelPermalink }} 480w,
                    {{ $webpMedium.RelPermalink }} 768w,
                    {{ $webpLarge.RelPermalink }} 1200w"
            sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
            type="image/webp">
    <img src="{{ $image.RelPermalink }}"
         alt="{{ .Text }}"
         loading="lazy"
         decoding="async"
         width="{{ $image.Width }}"
         height="{{ $image.Height }}">
  </picture>
{{ else }}
  <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" loading="lazy">
{{ end }}

Expected behavior: For every image referenced in Markdown content, Hugo generates three WebP sizes (480w, 768w, 1200w) and three AVIF sizes at slightly lower quality. The browser selects the best format and size combination. Unsupported formats fall back to the original image.

Responsive Images with Next.js

Next.js provides a built-in Image component that handles optimization, responsive sizes, and lazy loading automatically.

Next.js Image Configuration

// next.config.js -- Image optimization configuration
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [480, 768, 1024, 1280, 1536],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.ctfassets.net',
        pathname: '/**',
      },
    ],
  },
};
// components/OptimizedImage.js -- Usage in a component
import Image from 'next/image';

export default function OptimizedImage({ src, alt, priority = false }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={1200}
      height={675}
      sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
      priority={priority}
      loading={priority ? undefined : 'lazy'}
      quality={80}
    />
  );
}

Expected behavior: At build time for static export (output: 'export'), Next.js generates WebP and AVIF variants at the specified device sizes. The sizes attribute tells the browser which size to select at each viewport width. The priority prop disables lazy loading for above-the-fold images.

Automated Image Pipeline Script

For SSGs without built-in image processing, use a standalone script with Sharp or Squoosh CLI.

Sharp-Based Pipeline

// scripts/optimize-images.js -- Node.js image pipeline
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

const SOURCE_DIR = './src/images';
const OUTPUT_DIR = './static/images';
const BREAKPOINTS = [480, 768, 1200];
const QUALITY = 80;

async function optimizeImage(filePath) {
  const relativePath = path.relative(SOURCE_DIR, filePath);
  const parsed = path.parse(relativePath);
  const image = sharp(filePath);
  const metadata = await image.metadata();

  for (const width of BREAKPOINTS) {
    if (metadata.width < width) continue;

    const baseName = `${parsed.name}-${width}w`;

    // WebP
    await image
      .resize({ width })
      .webp({ quality: QUALITY })
      .toFile(path.join(OUTPUT_DIR, `${baseName}.webp`));

    // AVIF
    await image
      .resize({ width })
      .avif({ quality: QUALITY - 10 })
      .toFile(path.join(OUTPUT_DIR, `${baseName}.avif`));
  }
}

async function processDirectory(dir) {
  const entries = fs.readdirSync(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      await processDirectory(fullPath);
    } else if (/\.(jpg|jpeg|png)$/i.test(entry.name)) {
      await optimizeImage(fullPath);
    }
  }
}

processDirectory(SOURCE_DIR).then(() => {
  console.log('Image optimization complete');
});

Expected behavior: Running node scripts/optimize-images.js scans src/images/, generates WebP and AVIF variants at three breakpoints for each source image, and outputs them to static/images/. The script skips images smaller than the target breakpoint width.

CDN-Level Optimization

CDNs like Cloudflare and Akamai can perform on-the-fly image optimization without build-time processing.

Cloudflare Image Resizing

# wrangler.toml -- Enable Cloudflare Image Resizing
name = "image-optimizer"

[wasm_modules]
  image_resize = "image-resize.wasm"

[[headers]]
  for = "/images/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[image_resizing]]
  pattern = "/cdn-cgi/image/*"
// Generate image URLs with Cloudflare Image Resizing
function optimizedImageUrl(src, width, format = 'webp') {
  const base = 'https://your-site.com/cdn-cgi/image';
  return `${base}/width=${width},format=${format},quality=80/${src}`;
}

// Usage
const imgUrl = optimizedImageUrl('/images/hero.jpg', 1200, 'avif');
// Result: https://your-site.com/cdn-cgi/image/width=1200,format=avif,quality=80/images/hero.jpg

Expected behavior: When a request matches the image resizing pattern, Cloudflare resizes and converts the image on-the-fly and caches the result at the edge. Subsequent requests serve the cached version instantly.

Common Errors

1. Serving Oversized Images for Mobile

Serving a 4000px-wide hero image to a 375px mobile screen wastes 90% of bandwidth. Always set sizes attributes correctly and generate appropriately sized images for each breakpoint.

2. Missing loading="lazy" on Below-the-Fold Images

Without lazy loading, the browser downloads all images on page load, even those the user must scroll to see. Add loading="lazy" and decoding="async" to all non-critical images.

3. Forgetting Alt Text for Accessibility

Images without alt attributes fail WCAG accessibility guidelines and hurt SEO. Every <img> tag must have a descriptive alt attribute that conveys the image content.

4. Not Setting Explicit Width and Height

Without explicit dimensions, the browser cannot reserve space for images before they load, causing Cumulative Layout Shift (CLS). Always set width and height attributes matching the intrinsic dimensions.

5. Over-Compressing to the Point of Visible Artifacts

Aggressive compression (quality below 50) introduces blocky artifacts, color banding, and blur. At DodaTech, we find quality 75-82 for WebP and 65-75 for AVIF provides the best balance of file size and visual quality.

6. Not Handling Browser Support Gracefully

AVIF has ~80% browser support. Always provide WebP as a fallback and the original JPEG/PNG as a last resort. The <picture> element handles fallback order naturally.

Practice Questions

1. Which modern image format provides the best compression ratio?

AVIF typically provides 50-60% smaller files than JPEG at equivalent quality, making it the current leader in compression efficiency. However, it has slightly lower browser support (~80%) compared to WebP (~96%).

2. Why is the sizes attribute important in responsive images?

The sizes attribute tells the browser which image size to select from the srcset at each viewport width. Without it, the browser guesses (often incorrectly) and may download an image larger than needed.

3. What is the purpose of loading="lazy" and decoding="async"?

loading="lazy" defers loading of off-screen images until the user scrolls near them. decoding="async" tells the browser to decode the image asynchronously, preventing the main thread from blocking during image decoding.

4. How can you prevent Cumulative Layout Shift (CLS) with images?

Set explicit width and height attributes on every <img> tag, matching the image's intrinsic dimensions. This allows the browser to reserve the correct space before the image loads.

5. Challenge: Calculate the bandwidth savings of serving WebP vs JPEG for a site with 50 images averaging 400KB each, serving 10,000 page views per day.

JPEG weight: 50 * 400KB = 20MB per page load. WebP at 65% size: 50 * 260KB = 13MB. Daily savings: (20MB - 13MB) * 10,000 = 70GB. Annual savings: ~25 TB.

Mini Project: Automated Image Optimization Pipeline

Create a complete image optimization pipeline for a static site:

  1. Write a script (Node.js or Python) that:
    • Scans all images in content/ and static/
    • Converts JPEG and PNG to WebP and AVIF
    • Generates responsive sizes (480w, 768w, 1200w)
    • Updates Markdown references to point to optimized paths
  2. Integrate the script into the build pipeline (runs before hugo)
  3. Add a <picture> shortcode for easy use in content
  4. Verify the results with Lighthouse and WebPageTest
{{/* layouts/shortcodes/img.html */}}
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" }}
{{ $width := .Get "width" | default 1200 }}
{{ $page := .Page }}

{{ with $page.Resources.GetMatch (printf "**%s*" $src) }}
  <picture>
    <source srcset="{{ .Resize (printf "%dx avif q70" $width).RelPermalink }}" type="image/avif">
    <source srcset="{{ .Resize (printf "%dx webp q80" $width).RelPermalink }}" type="image/webp">
    <img src="{{ .RelPermalink }}"
         alt="{{ $alt }}"
         loading="lazy"
         decoding="async"
         width="{{ .Width }}"
         height="{{ .Height }}">
  </picture>
{{ end }}

Usage in Markdown: ![](screenshot.png "Dashboard screenshot")

Test the pipeline by adding a 5MB screenshot to a tutorial page and verifying the optimized output loads in under 200KB with no visible quality loss.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro