performance2024-01-10ยท10 minยท329/348

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 large

Solution

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 = nextConfig

Custom 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 */}
{alt}
{/* Desktop image */}
{alt}
) }

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

  1. Always include sizes attribute. Without sizes, Next.js sends the largest possible image for all viewports, negating the performance benefits.

  2. Use priority for above-the-fold images. This adds a <link rel="preload"> tag, preventing layout shift and improving LCP.

  3. AVIF provides 50% better compression than WebP. Enable both formats in the formats array.

  4. Set minimumCacheTTL appropriately. The default 60 seconds is too short for most use cases. 30 days is reasonable for most images.

  5. Monitor Core Web Vitals. Use Largest Contentful Paint and Cumulative Layout Shift metrics to validate image optimization effectiveness.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.