next.config.js Image Optimization Configuration Deep Dive
Complete guide to configuring image optimization in next.config.js including custom loaders, format selection, and device sizes
next.config.js Image Optimization Configuration Deep Dive
Introduction
Next.js image optimization is powerful out of the box, but the default configuration is not optimal for every project. Understanding each configuration option allows you to significantly reduce bandwidth, improve loading times, and control exactly how images are processed.
After optimizing a media-heavy application that served over 100,000 images daily, I reduced bandwidth costs by 60% through proper image optimization configuration. This post explains every relevant option and when to use each.
Environment
- OS: Windows 11
- Node.js: v20.10.0
- Next.js: 14.0.4
- Image CDN: Cloudinary (custom loader)
Problem
Default image optimization had several issues:
# Default config does not generate AVIF
# Only WebP and original format
# Default device sizes miss common breakpoints
# Result: Larger images sent to tablets and small laptops
# No caching headers for optimized images
# Every request re-optimizes the same image# Network tab shows:
GET /_next/image?url=/hero.jpg&w=3840&q=75 200 850ms 450KB
# 450KB for a hero image on mobile is too largeSolution
Complete image optimization configuration:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// Enable modern formats
formats: ['image/avif', 'image/webp'],
// Device widths to generate images for
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// Image widths for fixed-size images
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Minimum cache TTL (in seconds)
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
// Enable dangerouslyAllowSVG for SVG optimization
dangerouslyAllowSVG: false,
// Content Disposition header for downloaded images
contentDispositionType: 'inline',
// Content Security Policy for images
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
// Disable static imports for images
disableStaticImages: false,
// Custom loader (optional)
// loader: 'custom',
// loaderFile: './lib/imageLoader.ts',
// Remote patterns for external images
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '*.cloudinary.com',
pathname: '/my-app/**',
},
{
protocol: 'https',
hostname: 'images.ctfassets.net',
},
],
},
}
module.exports = nextConfigCustom loader for Cloudinary:
// lib/cloudinaryLoader.ts
interface CloudinaryLoaderParams {
src: string
width: number
quality?: number
}
export default function cloudinaryLoader({
src,
width,
quality = 75,
}: CloudinaryLoaderParams): string {
const params = [
'f_auto',
'c_limit',
`w_${width}`,
`q_${quality}`,
]
// If src already contains cloudinary URL, transform it
if (src.includes('cloudinary.com')) {
const parts = src.split('/')
const uploadIndex = parts.indexOf('upload')
if (uploadIndex !== -1) {
parts.splice(uploadIndex + 1, 0, params.join(','))
return parts.join('/')
}
}
// For other images, use cloudinary fetch
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`
}Responsive image component with art direction:
// components/ResponsiveHero.tsx
import Image from 'next/image'
interface ResponsiveHeroProps {
desktopSrc: string
mobileSrc: string
alt: string
}
export function ResponsiveHero({
desktopSrc,
mobileSrc,
alt,
}: ResponsiveHeroProps) {
return (
{/* Mobile image */}
{/* Desktop image */}
)
}Image performance monitoring:
// lib/imageMetrics.ts
export function trackImagePerformance(src: string, startTime: number) {
if (typeof window === 'undefined') return
const loadTime = performance.now() - startTime
// Send to analytics
if (navigator.sendBeacon) {
navigator.sendBeacon(
'/api/analytics/image',
JSON.stringify({
src,
loadTime,
timestamp: Date.now(),
connection: navigator.connection?.effectiveType || 'unknown',
})
)
}
}Lessons Learned
Always include
sizesattribute. Withoutsizes, Next.js sends the largest possible image for all viewports, negating the performance benefits.Use
priorityfor above-the-fold images. This adds a<link rel="preload">tag, preventing layout shift and improving LCP.AVIF provides 50% better compression than WebP. Enable both formats in the
formatsarray.Set
minimumCacheTTLappropriately. The default 60 seconds is too short for most use cases. 30 days is reasonable for most images.Monitor Core Web Vitals. Use
Largest Contentful PaintandCumulative Layout Shiftmetrics to validate image optimization effectiveness.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.