Skip to content

SSG Image Optimization — Optimizing Images for Static Sites

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SSG Image Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.

Image optimization for SSGs involves responsive images, modern formats, lazy loading, and build-time transformation pipelines for fast page loads.

What You'll Learn

By the end of this tutorial, you'll understand responsive image techniques, modern image formats (WebP, AVIF), build-time image processing pipelines, lazy loading strategies, and how to implement these in major SSG frameworks.

Why It Matters

Images account for 50-70% of a typical page's weight. Unoptimized images undo the speed benefits of SSG. Proper image optimization is the single highest-impact performance improvement for static sites.

Real-World Use

A photography portfolio SSG processes 1000+ images at build time. Each image generates 5 sizes in WebP and AVIF formats, with blurred placeholders. The site loads images only when scrolled into view, achieving a perfect Lighthouse performance score.

Image Optimization Pipeline

graph LR
    A[Source Images
JPEG / PNG] --> B[Build-time
Image Pipeline] B --> C[Resize to
multiple widths] B --> D[Convert to
WebP / AVIF] B --> E[Generate
blurred placeholders] B --> F[Generate
srcset attributes] C --> G[Responsive
image output] D --> G E --> H[Lazy loading
placeholder] F --> I[Optimized
HTML markup] G --> I H --> I I --> J[CDN Deploy] style B fill:#4a90d9,color:#fff style I fill:#e67e22,color:#fff style J fill:#27ae60,color:#fff

Build-Time Image Processing

// scripts/optimize-images.js — Sharp-based image pipeline
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const glob = require('glob');

const SIZES = [400, 800, 1200, 1600];
const FORMATS = ['webp', 'avif'];
const INPUT_DIR = './src/images';
const OUTPUT_DIR = './public/images';

if (!fs.existsSync(OUTPUT_DIR)) {
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}

async function optimizeImage(inputPath) {
    const filename = path.basename(inputPath, path.extname(inputPath));
    const metadata = await sharp(inputPath).metadata();

    // Generate responsive sizes and formats
    const outputs = [];

    for (const size of SIZES) {
        if (size > metadata.width) continue;

        for (const format of FORMATS) {
            const outputPath = path.join(
                OUTPUT_DIR,
                `${filename}-${size}.${format}`
            );

            await sharp(inputPath)
                .resize(size)
                .toFormat(format, {
                    quality: 80,
                    effort: 6,
                })
                .toFile(outputPath);

            outputs.push({
                path: outputPath,
                width: size,
                format,
                size: fs.statSync(outputPath).size,
            });
        }
    }

    // Generate placeholder (tiny blurred image)
    const placeholderPath = path.join(OUTPUT_DIR, `${filename}-placeholder.jpg`);
    await sharp(inputPath)
        .resize(20)
        .blur(5)
        .jpeg({ quality: 30 })
        .toFile(placeholderPath);

    console.log(`Optimized: ${filename} (${outputs.length} variants)`);
    return { filename, outputs, placeholder: placeholderPath };
}

// Process all images
const images = glob.sync(`${INPUT_DIR}/**/*.{jpg,jpeg,png}`);
Promise.all(images.map(optimizeImage))
    .then(results => {
        console.log(`\nOptimized ${results.length} images total.`);
        results.forEach(r => {
            const totalSize = r.outputs.reduce((s, o) => s + o.size, 0);
            console.log(`  ${r.filename}: ${(totalSize / 1024).toFixed(1)}KB total`);
        });
    });

Responsive Image Component

// components/OptimizedImage.jsx — Responsive image component
export default function OptimizedImage({
    src,
    alt,
    widths = [400, 800, 1200],
    className = '',
    priority = false,
}) {
    const basePath = src.replace(/\.(jpg|png)$/, '');

    // Generate srcset for WebP
    const webpSrcset = widths
        .map(w => `${basePath}-${w}.webp ${w}w`)
        .join(', ');

    // Generate srcset for fallback format
    const fallbackSrcset = widths
        .map(w => `${basePath}-${w}.jpg ${w}w`)
        .join(', ');

    return (
        <picture>
            {widths.map(w => (
                <source
                    key={w}
                    type="image/avif"
                    srcSet={`${basePath}-${w}.avif ${w}w`}
                    sizes="(max-width: 768px) 100vw, 50vw"
                />
            ))}
            <source
                type="image/webp"
                srcSet={webpSrcset}
                sizes="(max-width: 768px) 100vw, 50vw"
            />
            <img
                src={`${basePath}-${widths[0]}.jpg`}
                srcSet={fallbackSrcset}
                sizes="(max-width: 768px) 100vw, 50vw"
                alt={alt}
                loading={priority ? 'eager' : 'lazy'}
                fetchpriority={priority ? 'high' : 'auto'}
                decoding="async"
                className={className}
                width={widths[0]}
                height={Math.round(widths[0] * 0.5625)}
            />
        </picture>
    );
}

Next.js Image Optimization

// next.config.js — Built-in image optimization
module.exports = {
    images: {
        formats: ['image/avif', 'image/webp'],
        deviceSizes: [640, 750, 828, 1080, 1200, 1920],
        imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
        minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days

        // Remote image sources
        remotePatterns: [
            {
                protocol: 'https',
                hostname: 'images.ctfassets.net', // Contentful
            },
            {
                protocol: 'https',
                hostname: 'cdn.sanity.io',
            },
        ],
    },
};

// Usage in components
import Image from 'next/image';

export default function Hero({ post }) {
    return (
        <div className="hero">
            <Image
                src={post.imageUrl}
                alt={post.title}
                width={1200}
                height={675}
                priority // LCP image — loads eagerly
                quality={85}
                sizes="100vw"
                placeholder="blur"
                blurDataURL={post.placeholderBase64}
            />
            <h1 className="overlay">{post.title}</h1>
        </div>
    );
}

Gatsby Image Optimization

// gatsby-config.js — Sharp-based image plugin
module.exports = {
    plugins: [
        'gatsby-plugin-image',
        'gatsby-plugin-sharp',
        'gatsby-transformer-sharp',
        {
            resolve: 'gatsby-source-filesystem',
            options: {
                name: 'images',
                path: `${__dirname}/src/images`,
            },
        },
    ],
};

// Usage in components
import { GatsbyImage, getImage, getSrc } from 'gatsby-plugin-image';

export default function BlogPost({ data }) {
    const image = getImage(data.markdownRemark.frontmatter.featuredImage);

    return (
        <article>
            <GatsbyImage
                image={image}
                alt="Featured image"
                className="featured-image"
                loading="lazy"
            />
        </article>
    );
}

// GraphQL fragment for optimized images
export const query = graphql`
    query($slug: String!) {
        markdownRemark(fields: { slug: { eq: $slug } }) {
            frontmatter {
                featuredImage {
                    childImageSharp {
                        gatsbyImageData(
                            width: 1200
                            placeholder: BLURRED
                            formats: [AUTO, WEBP, AVIF]
                            layout: CONSTRAINED
                        )
                    }
                }
            }
        }
    }
`;

Common Mistakes

  1. Serving unoptimized original images. A 5MB JPEG from a camera should never be served directly. Resize and compress during build.
  2. Not using responsive images. Serving a 2000px image to a 375px mobile viewport wastes bandwidth. Use srcset and sizes attributes.
  3. Ignoring modern formats. WebP is 25-34% smaller than JPEG. AVIF is 50% smaller. Use elements with format fallbacks.
  4. Missing lazy loading. Images below the fold should lazy load. Use loading="lazy" or Intersection Observer.
  5. Not optimizing the Largest Contentful Paint (LCP) image. The hero image should load eagerly (loading="eager", fetchpriority="high") and be preloaded.

Practice Questions

  1. What image formats should you use for modern static sites?
  2. How do srcset and sizes attributes enable responsive images?
  3. What is the purpose of blur-up placeholders in image loading?
  4. How does Sharp Process images during an SSG build?
  5. What is the LCP image and how should it be optimized differently?

Challenge: Build an image optimization pipeline for a photography portfolio site: create a Sharp-based build script that generates responsive variants, implement a responsive component, add lazy loading with blur placeholders, and achieve a 95+ Lighthouse performance score.

FAQ

What is the best image format for the web?

AVIF offers the best compression (50% smaller than JPEG) but not all browsers support it. Use WebP as the primary format with JPEG fallback. AVIF as an enhancement.

How many image sizes should I generate?

4-6 sizes covering mobile (400px), tablet (800px), desktop (1200px), and large screens (1600px). Adjust based on your layout's image display sizes.

Should images be served from the same domain or a CDN?

A CDN is better for global performance. Most SSG deployments already use CDNs. Services like Cloudinary or imgix offer advanced transformation capabilities.

How do I optimize images from a CMS in an SSG?

Download CMS images during build, process them with Sharp, and serve the optimized versions. Many CMS platforms (Contentful, Cloudinary) offer built-in image CDNs.

What is the impact of image optimization on build time?

Significant for large sites. A 1000-image site may add 5-10 minutes to build time. Run image processing in parallel and cache processed images between builds.

Mini Project

Create an image optimization setup for a photo blog with 50 images: write a Sharp processing script (resize to 400/800/1200, convert to WebP/AVIF, generate placeholders), create a responsive React component, implement lazy loading, and verify the Lighthouse performance score.

What's Next

Images are optimized. Now organize content with SSG Pagination to split large content sets into manageable pages.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro