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
nextjs
image optimization
web performance
responsive images
frontend architecture
performance engineering
caching strategy
scalable frontend
modern web
developer experience
react
system design
optimization
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?
Related Posts
April 4, 202697 min
Next.js Proxy Deep Dive: Edge-First Request Interception
nextjs
proxy
April 4, 2026164 min
Next.js Route Handlers Deep Dive: Building Production APIs with Web Standards
nextjs
route handlers
April 4, 202691 min
Next.js Metadata and OG Images Deep Dive: SEO, Social Sharing, and Dynamic Generation
nextjs
seo