SSG Image Optimization — Optimizing Images for Static Sites
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
- Serving unoptimized original images. A 5MB JPEG from a camera should never be served directly. Resize and compress during build.
- Not using responsive images. Serving a 2000px image to a 375px mobile viewport wastes bandwidth. Use srcset and sizes attributes.
- Ignoring modern formats. WebP is 25-34% smaller than JPEG. AVIF is 50% smaller. Use
elements with format fallbacks. - Missing lazy loading. Images below the fold should lazy load. Use loading="lazy" or Intersection Observer.
- Not optimizing the Largest Contentful Paint (LCP) image. The hero image should load eagerly (loading="eager", fetchpriority="high") and be preloaded.
Practice Questions
- What image formats should you use for modern static sites?
- How do srcset and sizes attributes enable responsive images?
- What is the purpose of blur-up placeholders in image loading?
- How does Sharp Process images during an SSG build?
- 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
FAQ
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