NextJS DOC
Part 11 of 151. Next.js Project Structure: A Comprehensive Architecture Guide2. Next.js Layouts and Pages: Complete Architecture Guide3. Next.js Linking and Navigation: Complete Architecture Guide4. Next.js Server and Client Components: Complete Architecture Guide5. Next.js Data Fetching: Complete Architecture Guide6. Next.js Data Mutation: Complete Server Actions Guide7. Next.js Caching Deep Dive: Cache Components and the `use cache` Directive8. Next.js Revalidation Deep Dive: Time-Based and On-Demand Cache Invalidation9. Next.js Error Handling Deep Dive: Expected Errors, Uncaught Exceptions, and Recovery Patterns10. Next.js CSS Deep Dive: Tailwind, CSS Modules, Sass, and CSS-in-JS11. Next.js Image Optimization Deep Dive: Performance, Responsive Images, and Configuration12. Next.js Font Optimization Deep Dive: Self-Hosted Fonts, CSS Variables, and CLS Prevention13. Next.js Metadata and OG Images Deep Dive: SEO, Social Sharing, and Dynamic Generation14. Next.js Route Handlers Deep Dive: Building Production APIs with Web Standards15. Next.js Proxy Deep Dive: Edge-First Request Interception
Next.js Image Optimization Deep Dive: Performance, Responsive Images, and Configuration
April 2, 202670 min read0 views
Next.js Image Optimization Deep Dive: Performance, Responsive Images, and Configuration
Introduction
The Next.js <Image> component provides automatic image optimization including format conversion (WebP/AVIF), responsive sizing, lazy loading, and blur-up placeholders. This guide covers the internals of image optimization, configuration options, and production patterns for maximizing Core Web Vitals.
Image Optimization Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ NEXT.JS IMAGE OPTIMIZATION PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ IMAGE REQUEST FLOW │ │
│ │ │ │
│ │ Browser Request │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ /_next/image?url=...&w=640&q=75 │ │ │
│ │ │ │ │ │
│ │ │ Parameters: │ │ │
│ │ │ • url: Source image path │ │ │
│ │ │ • w: Target width (from deviceSizes/imageSizes) │ │ │
│ │ │ • q: Quality (1-100, from qualities array) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ OPTIMIZATION ENGINE │ │ │
│ │ │ │ │ │
│ │ │ 1. Check disk cache (LRU, configurable size) │ │ │
│ │ │ 2. If cache miss: │ │ │
│ │ │ a. Fetch source image │ │ │
│ │ │ b. Detect browser format support (Accept header) │ │ │
│ │ │ c. Resize to requested width │ │ │
│ │ │ d. Encode to WebP/AVIF/original │ │ │
│ │ │ e. Store in disk cache │ │ │
│ │ │ 3. Serve optimized image │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ Response Headers │ │ │
│ │ │ • Cache-Control: max-age=31536000 (static import) │ │ │
│ │ │ • Content-Type: image/webp (or avif/jpeg/png) │ │ │
│ │ │ • Content-Disposition: attachment (SVG security) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ GENERATED HTML OUTPUT │ │
│ │ │ │
│ │ <img │ │
│ │ srcset="/_next/image?url=...&w=640&q=75 640w, │ │
│ │ /_next/image?url=...&w=750&q=75 750w, │ │
│ │ /_next/image?url=...&w=828&q=75 828w, │ │
│ │ /_next/image?url=...&w=1080&q=75 1080w, │ │
│ │ /_next/image?url=...&w=1200&q=75 1200w" │ │
│ │ sizes="(max-width: 768px) 100vw, 50vw" │ │
│ │ src="/_next/image?url=...&w=1200&q=75" │ │
│ │ loading="lazy" │ │
│ │ decoding="async" │ │
│ │ /> │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Core Image Component Usage
Local Images (Static Import)
// app/page.tsx
import Image from 'next/image'
import heroImage from './hero.png' // Static import
export default function Page() {
return (
<Image
src={heroImage}
alt="Hero image"
// width and height automatically inferred
// blurDataURL automatically generated
placeholder="blur" // Shows blur while loading
/>
)
}
Remote Images
// app/page.tsx
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://cdn.example.com/photos/profile.jpg"
alt="Profile photo"
width={400} // Required for remote images
height={400} // Required for remote images
/>
)
}
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/photos/**',
},
],
},
}
export default nextConfig
Essential Props
Size and Layout
// Fixed size image
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
/>
// Fill container (responsive)
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/photo.jpg"
alt="Photo"
fill // Fills parent container
style={{ objectFit: 'cover' }} // cover, contain, none
/>
</div>
Responsive Images with sizes
// Hero image - full width on mobile, half on desktop
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, 50vw"
/>
// Grid item - third of viewport on large screens
<Image
src="/product.jpg"
alt="Product"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
How sizes affects srcset:
┌─────────────────────────────────────────────────────────────────────────────┐
│ sizes PROP BEHAVIOR │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT sizes: │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ srcset generated for 1x and 2x density only │ │
│ │ Browser assumes image is 100vw │ │
│ │ May download unnecessarily large images │ │
│ │ │ │
│ │ srcset="...&w=640 1x, ...&w=828 2x" │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ WITH sizes="(max-width: 768px) 100vw, 50vw": │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Full srcset generated for all deviceSizes │ │
│ │ Browser selects optimal size based on viewport │ │
│ │ Significant bandwidth savings on large screens │ │
│ │ │ │
│ │ srcset="...&w=640 640w, ...&w=750 750w, ...&w=828 828w, │ │
│ │ ...&w=1080 1080w, ...&w=1200 1200w, ..." │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Loading and Priority
// Above-the-fold hero image - preload
<Image
src="/hero.jpg"
alt="Hero"
fill
preload={true} // Preloads via <link> in <head>
loading="eager" // Load immediately
/>
// Below-the-fold content - lazy load (default)
<Image
src="/feature.jpg"
alt="Feature"
width={600}
height={400}
loading="lazy" // Default - loads when entering viewport
/>
Placeholders
// Blur placeholder (auto-generated for static imports)
import photo from './photo.jpg'
<Image
src={photo}
alt="Photo"
placeholder="blur"
/>
// Custom blur placeholder for remote images
<Image
src="https://cdn.example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
/>
// Color placeholder
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/svg+xml;base64,PHN2ZyB4bWxucz0ia..."
/>
Generating blur placeholders:
// Using plaiceholder library
import { getPlaiceholder } from 'plaiceholder'
import fs from 'fs/promises'
async function getBlurDataURL(imagePath: string) {
const buffer = await fs.readFile(imagePath)
const { base64 } = await getPlaiceholder(buffer)
return base64
}
// Simple color placeholder
function getColorPlaceholder(r: number, g: number, b: number) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect fill="rgb(${r},${g},${b})" width="1" height="1"/></svg>`
return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
}
Configuration Options
Complete Configuration Reference
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
// Remote image security
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com', // Wildcard subdomain
pathname: '/images/**',
},
],
// Local image security
localPatterns: [
{
pathname: '/assets/images/**',
},
],
// Device breakpoints for srcset generation
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// Small image sizes (when sizes prop < smallest deviceSize)
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Allowed quality values (required in Next.js 16+)
qualities: [25, 50, 75, 100],
// Output formats (order matters - first supported wins)
formats: ['image/avif', 'image/webp'],
// Cache TTL in seconds
minimumCacheTTL: 2678400, // 31 days
// Disk cache size limit in bytes
maximumDiskCacheSize: 500_000_000, // 500 MB
// Max source image size
maximumResponseBody: 50_000_000, // 50 MB
// Max redirects to follow
maximumRedirects: 3,
// Disable optimization globally
unoptimized: false,
// Custom image path
path: '/_next/image',
// SVG handling (security sensitive)
dangerouslyAllowSVG: false,
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
}
export default nextConfig
Custom Loader
// lib/image-loader.ts
'use client'
import type { ImageLoaderProps } from 'next/image'
export default function cloudinaryLoader({
src,
width,
quality,
}: ImageLoaderProps) {
const params = [
'f_auto',
'c_limit',
`w_${width}`,
`q_${quality || 'auto'}`,
]
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`
}
// next.config.ts
const nextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
},
}
// Or per-image loader
import Image from 'next/image'
import cloudinaryLoader from '@/lib/image-loader'
<Image
loader={cloudinaryLoader}
src="/photos/my-image.jpg"
alt="Photo"
width={800}
height={600}
/>
Production Patterns
Pattern 1: Responsive Hero Image
// components/Hero.tsx
import Image from 'next/image'
interface HeroProps {
title: string
imageSrc: string
imageAlt: string
}
export function Hero({ title, imageSrc, imageAlt }: HeroProps) {
return (
<section className="relative h-[60vh] min-h-[400px]">
<Image
src={imageSrc}
alt={imageAlt}
fill
sizes="100vw"
style={{ objectFit: 'cover' }}
preload // Critical LCP image
loading="eager"
quality={85}
/>
{/* Dark overlay for text readability */}
<div className="absolute inset-0 bg-black/40" />
<div className="relative z-10 flex h-full items-center justify-center">
<h1 className="text-5xl font-bold text-white">{title}</h1>
</div>
</section>
)
}
Pattern 2: Product Image Gallery
// components/ProductGallery.tsx
'use client'
import Image from 'next/image'
import { useState } from 'react'
interface ProductGalleryProps {
images: Array<{
src: string
alt: string
blurDataURL?: string
}>
}
export function ProductGallery({ images }: ProductGalleryProps) {
const [selectedIndex, setSelectedIndex] = useState(0)
return (
<div className="flex flex-col gap-4">
{/* Main image */}
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
<Image
src={images[selectedIndex].src}
alt={images[selectedIndex].alt}
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'contain' }}
placeholder={images[selectedIndex].blurDataURL ? 'blur' : 'empty'}
blurDataURL={images[selectedIndex].blurDataURL}
preload={selectedIndex === 0} // Preload first image
/>
</div>
{/* Thumbnails */}
<div className="flex gap-2 overflow-x-auto">
{images.map((image, index) => (
<button
key={image.src}
onClick={() => setSelectedIndex(index)}
className={`relative h-20 w-20 flex-shrink-0 overflow-hidden rounded ${
index === selectedIndex ? 'ring-2 ring-blue-500' : ''
}`}
>
<Image
src={image.src}
alt={`Thumbnail ${index + 1}`}
fill
sizes="80px"
style={{ objectFit: 'cover' }}
/>
</button>
))}
</div>
</div>
)
}
Pattern 3: Lazy-Loaded Image Grid
// components/ImageGrid.tsx
import Image from 'next/image'
interface ImageGridProps {
images: Array<{
id: string
src: string
alt: string
width: number
height: number
blurDataURL?: string
}>
}
export function ImageGrid({ images }: ImageGridProps) {
return (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
{images.map((image, index) => (
<div
key={image.id}
className="relative aspect-square overflow-hidden rounded-lg"
>
<Image
src={image.src}
alt={image.alt}
fill
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
style={{ objectFit: 'cover' }}
placeholder={image.blurDataURL ? 'blur' : 'empty'}
blurDataURL={image.blurDataURL}
// First 4 images load eagerly (above fold)
loading={index < 4 ? 'eager' : 'lazy'}
/>
</div>
))}
</div>
)
}
Pattern 4: Art Direction (Different Images per Breakpoint)
// components/ResponsiveBanner.tsx
import { getImageProps } from 'next/image'
interface ResponsiveBannerProps {
desktopSrc: string
mobileSrc: string
alt: string
}
export function ResponsiveBanner({
desktopSrc,
mobileSrc,
alt,
}: ResponsiveBannerProps) {
const common = { alt, sizes: '100vw' }
const {
props: { srcSet: desktopSrcSet },
} = getImageProps({
...common,
src: desktopSrc,
width: 1920,
height: 600,
quality: 85,
})
const {
props: { srcSet: mobileSrcSet, ...rest },
} = getImageProps({
...common,
src: mobileSrc,
width: 750,
height: 1000,
quality: 80,
})
return (
<picture>
<source media="(min-width: 768px)" srcSet={desktopSrcSet} />
<source media="(max-width: 767px)" srcSet={mobileSrcSet} />
<img
{...rest}
style={{ width: '100%', height: 'auto' }}
loading="eager"
/>
</picture>
)
}
Pattern 5: Theme-Aware Images
// components/ThemeImage.tsx
import Image, { type ImageProps } from 'next/image'
import styles from './ThemeImage.module.css'
type ThemeImageProps = Omit<ImageProps, 'src'> & {
srcLight: string
srcDark: string
}
export function ThemeImage({ srcLight, srcDark, alt, ...props }: ThemeImageProps) {
return (
<>
<Image
{...props}
src={srcLight}
alt={alt}
className={styles.light}
/>
<Image
{...props}
src={srcDark}
alt={alt}
className={styles.dark}
/>
</>
)
}
/* ThemeImage.module.css */
.dark {
display: none;
}
@media (prefers-color-scheme: dark) {
.light {
display: none;
}
.dark {
display: block;
}
}
Pattern 6: Background Image with Optimization
// components/BackgroundSection.tsx
import { getImageProps } from 'next/image'
interface BackgroundSectionProps {
imageSrc: string
children: React.ReactNode
}
function getBackgroundImage(srcSet: string) {
const imageSet = srcSet
.split(', ')
.map((str) => {
const [url, dpi] = str.split(' ')
return `url("${url}") ${dpi}`
})
.join(', ')
return `image-set(${imageSet})`
}
export function BackgroundSection({
imageSrc,
children,
}: BackgroundSectionProps) {
const {
props: { srcSet },
} = getImageProps({
alt: '',
src: imageSrc,
width: 1920,
height: 1080,
quality: 80,
})
const backgroundImage = getBackgroundImage(srcSet!)
return (
<section
style={{
backgroundImage,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
className="relative min-h-[500px]"
>
<div className="absolute inset-0 bg-black/50" />
<div className="relative z-10">{children}</div>
</section>
)
}
Performance Optimization
LCP Optimization Checklist
┌─────────────────────────────────────────────────────────────────────────────┐
│ LCP IMAGE OPTIMIZATION CHECKLIST │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ✓ Use preload={true} for hero images │
│ ✓ Use loading="eager" for above-the-fold images │
│ ✓ Avoid loading="lazy" on LCP images │
│ ✓ Provide accurate sizes prop for responsive images │
│ ✓ Use static imports for auto width/height │
│ ✓ Enable AVIF for best compression: formats: ['image/avif', 'image/webp']│
│ ✓ Preconnect to external image domains: │
│ <link rel="preconnect" href="https://cdn.example.com" /> │
│ ✓ Avoid placeholder="blur" on LCP images (adds decode time) │
│ ✓ Use appropriate quality (75-85 for photos, higher for text) │
│ │
│ MEASURING LCP: │
│ • Chrome DevTools → Performance → Web Vitals │
│ • Lighthouse → Performance audit │
│ • Real User Monitoring (RUM) via web-vitals library │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
CLS Prevention
// Always provide dimensions to prevent layout shift
// Option 1: Static import (auto dimensions)
import heroImage from './hero.jpg'
<Image src={heroImage} alt="Hero" />
// Option 2: Explicit dimensions
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
/>
// Option 3: Fill with sized container
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
<Image src="/photo.jpg" alt="Photo" fill />
</div>
// Option 4: CSS aspect ratio
<div className="relative aspect-video">
<Image src="/video-thumb.jpg" alt="Video" fill />
</div>
Common Pitfalls and Solutions
Pitfall 1: Missing remotePatterns
// ❌ ERROR: Invalid src prop on next/image
<Image src="https://external-site.com/image.jpg" ... />
// ✅ Configure remotePatterns
// next.config.ts
{
images: {
remotePatterns: [
{ hostname: 'external-site.com' }
]
}
}
Pitfall 2: Missing Width/Height for Remote Images
// ❌ ERROR: Missing width or height
<Image src="https://cdn.example.com/photo.jpg" alt="Photo" />
// ✅ Provide dimensions
<Image
src="https://cdn.example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
/>
// ✅ Or use fill
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="https://cdn.example.com/photo.jpg"
alt="Photo"
fill
/>
</div>
Pitfall 3: Fill Without Positioned Parent
// ❌ Image won't display correctly
<div>
<Image src="/photo.jpg" alt="Photo" fill />
</div>
// ✅ Parent must have position
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image src="/photo.jpg" alt="Photo" fill />
</div>
Pitfall 4: Missing sizes for Responsive Images
// ❌ Browser downloads 100vw image even on small displays
<div className="w-1/3">
<Image src="/photo.jpg" alt="Photo" fill />
</div>
// ✅ Provide sizes to optimize srcset selection
<div className="w-full md:w-1/2 lg:w-1/3">
<Image
src="/photo.jpg"
alt="Photo"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
Pitfall 5: Quality Not in Allowlist
// ❌ ERROR: Quality 90 not in qualities array
<Image src="/photo.jpg" alt="Photo" width={800} height={600} quality={90} />
// ✅ Configure allowed qualities
// next.config.ts
{
images: {
qualities: [25, 50, 75, 90, 100] // Add 90 to allowlist
}
}
Key Takeaways
- Static Imports for Local Images: Auto width/height/blur - best DX and performance
- Always Provide sizes for fill Images: Critical for responsive srcset selection
- preload for LCP Images: Use for hero images above the fold
- Configure remotePatterns Strictly: Security first - be specific with patterns
- Enable AVIF: 20% smaller than WebP with
formats: ['image/avif', 'image/webp'] - quality 75-85 for Photos: Balance between file size and visual quality
- Use fill for Unknown Dimensions: Combined with objectFit for flexible layouts
- Avoid Lazy Loading LCP: Use
loading="eager"for above-the-fold images - getImageProps for Advanced Use: Art direction, background images, custom elements
- Custom Loaders for CDNs: Cloudinary, Imgix, etc. - use loader prop or loaderFile
References
What did you think?