Overview
Production-grade image optimization pipeline: automatic resizing, format conversion (WebP/AVIF), thumbnail generation, lazy loading, and responsive images. Reduces page load by 60-80% while maintaining visual quality.
Use Cases
- E-commerce: Product photos with multiple sizes
- Social platforms: User avatars, cover images, photo feeds
- Blogs/CMS: Article images with responsive breakpoints
- Marketing sites: Hero images, testimonials, team photos
When to Use This Pattern
Use this pattern when you need to:
- Serve images at optimal sizes for different devices
- Reduce bandwidth and improve page load speed
- Generate thumbnails automatically on upload
- Support modern formats (WebP, AVIF) with fallbacks
- Implement lazy loading for below-the-fold images
- Deliver images via CDN for global performance
Pro Tips
Before you start implementing, read these carefully:
- Next.js Image component handles 90% automatically - Use it unless you need custom logic
- Generate thumbnails on upload, not on request - Faster UX, no waiting
- Always provide width/height - Prevents layout shift, improves Core Web Vitals
- Use WebP with JPEG fallback - 30% smaller, 95% browser support
- Lazy load below-the-fold images - Saves bandwidth, faster initial load
Implementation Phases
Phase 1: Next.js Image Component (Easiest)
Use Next.js built-in optimization:
import Image from 'next/image'
export default function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
quality={85}
loading="lazy"
className="rounded-lg"
/>
)
}
What Next.js Image does automatically:
- ā Resizes to optimal size for device
- ā Converts to WebP/AVIF (modern formats)
- ā Lazy loads by default
- ā Prevents layout shift
- ā Serves via CDN (Vercel)
- ā Blur-up placeholder
Configure in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**.supabase.co',
},
{
protocol: 'https',
hostname: '**.cloudinary.com',
},
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
module.exports = nextConfig
Phase 2: Server-Side Thumbnail Generation
Generate thumbnails on upload with Sharp:
npm install sharp
// app/api/upload/route.ts
import sharp from 'sharp'
import { put } from '@vercel/blob'
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get('file') as File
const buffer = Buffer.from(await file.arrayBuffer())
// Generate multiple sizes
const thumbnail = await sharp(buffer)
.resize(150, 150, { fit: 'cover' })
.webp({ quality: 80 })
.toBuffer()
const medium = await sharp(buffer)
.resize(800, 800, { fit: 'inside' })
.webp({ quality: 85 })
.toBuffer()
const large = await sharp(buffer)
.resize(1920, 1920, { fit: 'inside' })
.webp({ quality: 90 })
.toBuffer()
// Upload all sizes
const [thumbnailBlob, mediumBlob, largeBlob] = await Promise.all([
put(`thumbnails/${file.name}`, thumbnail, { access: 'public' }),
put(`medium/${file.name}`, medium, { access: 'public' }),
put(`large/${file.name}`, large, { access: 'public' }),
])
// Save URLs to database
await prisma.image.create({
data: {
original: largeBlob.url,
medium: mediumBlob.url,
thumbnail: thumbnailBlob.url,
userId: session.user.id,
},
})
return NextResponse.json({
thumbnail: thumbnailBlob.url,
medium: mediumBlob.url,
large: largeBlob.url,
})
}
Phase 3: Responsive Images Component
Build reusable image component:
'use client'
import { useState } from 'react'
import Image from 'next/image'
interface ResponsiveImageProps {
src: string
alt: string
thumbnail?: string
width: number
height: number
priority?: boolean
}
export default function ResponsiveImage({
src,
alt,
thumbnail,
width,
height,
priority = false
}: ResponsiveImageProps) {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative overflow-hidden rounded-lg bg-stone-100">
{/* Blur placeholder (thumbnail) */}
{thumbnail && isLoading && (
<Image
src={thumbnail}
alt=""
fill
className="blur-sm"
/>
)}
{/* Full image */}
<Image
src={src}
alt={alt}
width={width}
height={height}
loading={priority ? 'eager' : 'lazy'}
priority={priority}
onLoadingComplete={() => setIsLoading(false)}
className={`
transition-opacity duration-300
${isLoading ? 'opacity-0' : 'opacity-100'}
`}
/>
</div>
)
}
Phase 4: CDN Integration
Option A: Cloudinary (Best for complex transformations)
// lib/cloudinary.ts
export function getCloudinaryUrl(
publicId: string,
options: {
width?: number
height?: number
quality?: number
format?: 'auto' | 'webp' | 'avif'
} = {}
) {
const { width, height, quality = 'auto', format = 'auto' } = options
const transformations = [
width && `w_${width}`,
height && `h_${height}`,
`q_${quality}`,
`f_${format}`,
'c_limit', // Don't upscale
].filter(Boolean).join(',')
return `https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/${transformations}/${publicId}`
}
// Usage
<Image
src={getCloudinaryUrl('user-avatar', { width: 150, height: 150, format: 'webp' })}
alt="User avatar"
width={150}
height={150}
/>
Option B: Imgix (Best for real-time transformations)
// lib/imgix.ts
export function getImgixUrl(
path: string,
options: {
width?: number
height?: number
quality?: number
format?: 'auto' | 'webp' | 'avif'
} = {}
) {
const { width, height, quality = 75, format = 'auto' } = options
const params = new URLSearchParams({
...(width && { w: width.toString() }),
...(height && { h: height.toString() }),
q: quality.toString(),
fm: format,
fit: 'crop',
auto: 'format,compress',
})
return `https://${process.env.IMGIX_DOMAIN}/${path}?${params}`
}
Edge Cases to Handle
Layout shift (CLS):
- Image loads, page jumps
- Solution: Always provide width/height, use aspect-ratio CSS
Broken image links:
- Image deleted from storage but URL in database
- Solution: Fallback image, error boundary
<Image
src={src}
alt={alt}
width={800}
height={600}
onError={(e) => {
e.currentTarget.src = '/placeholder.png'
}}
/>
Large images on mobile:
- Serving 4K image to phone
- Solution: Responsive sizes attribute
sizes="(max-width: 768px) 100vw, 50vw"
Tech Stack Recommendations
Minimum Viable Stack
- Framework: Next.js with Image component
- Storage: Vercel Blob or Cloudinary
- No additional packages needed
Production-Grade Stack
- Framework: Next.js 15 with Image component
- Processing: Sharp for server-side resizing
- CDN: Cloudinary or Imgix for transformations
- Storage: S3 + CloudFront or Vercel Blob
- Monitoring: Cloudflare Image Analytics
Full Implementation Prompt
Copy this prompt to use with Claude Code:
I need to implement image optimization with thumbnails, lazy loading, and responsive images. Before we start, help me review my setup.
First, let's understand my use case:
- How many images does my app handle? (Dozens, hundreds, thousands per day?)
- Are images user-uploaded or admin-curated?
- Do I need real-time transformations or can I pre-generate sizes?
- Am I using Next.js? (Makes optimization much easier)
- Do I already have a CDN or image service? (Cloudinary, Imgix, Vercel)
Then we'll implement: Phase 1: Configure Next.js Image component (if using Next.js) Phase 2: Set up Sharp for thumbnail generation on upload Phase 3: Build responsive image component with blur placeholders Phase 4: Configure CDN for global delivery Phase 5: Implement lazy loading for performance
After implementation, test:
- Images load at correct sizes for mobile/desktop
- WebP format served to modern browsers
- Lazy loading works (check Network tab)
- No layout shift (check Lighthouse CLS score)
- Thumbnails generated on upload
Sound good? Let's start with your image optimization setup.
Related Feature Specs
- File Upload Flow - Upload images with drag-and-drop
- Video Upload - Optimize video delivery
Success Criteria
ā Images load at optimal sizes for each device ā Modern formats (WebP/AVIF) served automatically ā Thumbnails generated on upload ā Lazy loading below-the-fold images ā No layout shift (CLS < 0.1) ā Fast load times (<2s LCP)
Common Mistakes to Avoid
ā Not providing width/height (causes layout shift) ā Loading full-size images on mobile ā Not using lazy loading ā Serving PNG/JPEG only (no WebP) ā Client-side resizing (slow, uses data) ā Not using CDN (slow for global users)
Last Updated: 2025-12-04 Difficulty: Intermediate Estimated Time: 3-5 hours Prerequisites: File upload basics, Next.js Image component knowledge