
TLDR
- Use
next/image
for large images (>40x40px) that need optimization, responsive behavior, or lazy loading - Don't use it for SVGs (unless configured), tiny icons, or decorative elements
- Key features:
<Image src="/image.jpg" alt="Description" width={800} // Required to prevent layout shift height={600} // Required to prevent layout shift priority // For above-the-fold images fill // For parent-based sizing sizes="(max-width: 768px) 100vw, 50vw" // For responsive images />
- Always wrap
fill
mode images in arelative
parent with defined dimensions - Use
priority
for hero images and critical above-the-fold content - Next.js v15 introduces improved
remotePatterns
with URL object support
As web developers, we often struggle with image optimization and responsive design. Next.js provides a powerful next/image
component that helps solve these challenges, but it can be tricky to understand when and how to use it effectively. In this guide, I'll break down the best practices and common patterns for using the Next.js Image component with Next.js v15.
When to Use next/image
The next/image
component is powerful but isn't always the right choice. Here's when you should and shouldn't use it:
Good Use Cases
1. Hero Images / Banner Images
function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero banner showcasing our main product"
width={1920}
height={1080}
priority // Load immediately as it's above the fold
className="w-full h-auto"
sizes="100vw" // Full viewport width on all devices
/>
);
}
2. Product Images with Modern Responsive Design
function ProductCard({ product }) {
return (
<div className="relative aspect-square">
<Image
src={product.imageUrl}
alt={`${product.name} - ${product.description}`}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover rounded-lg"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R+Rq" // Ultra-low quality placeholder
/>
</div>
);
}
3. Profile Pictures with Error Handling
'use client'
import { useState } from 'react'
import Image from 'next/image'
function Avatar({ user }) {
const [imageError, setImageError] = useState(false)
return (
<div className="relative w-12 h-12 rounded-full overflow-hidden bg-gray-100">
{!imageError ? (
<Image
src={user.avatar}
alt={`${user.name}'s profile picture`}
fill
sizes="48px"
className="object-cover"
onError={() => setImageError(true)}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-200 text-gray-500 text-xs">
{user.name?.charAt(0) || '?'}
</div>
)}
</div>
);
}
When Not to Use
Don't use next/image
for:
- SVG icons (unless you configure
dangerouslyAllowSVG
) - Small decorative images (<40x40 pixels)
- Images that don't benefit from optimization
- Simple logos or icons that are part of the UI chrome
Responsive Patterns
1. Fixed Aspect Ratio with CSS Grid
function ResponsiveImageGrid() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{images.map((image) => (
<div key={image.id} className="relative aspect-[4/3] bg-gray-100 rounded-lg overflow-hidden">
<Image
src={image.url}
alt={image.alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
))}
</div>
);
}
2. Art-directed Images with getImageProps (Next.js v14+)
import { getImageProps } from 'next/image'
function ArtDirectedHero() {
const common = {
alt: 'Hero showcasing our product in different contexts',
sizes: '100vw'
}
const {
props: { srcSet: desktop },
} = getImageProps({
...common,
width: 1920,
height: 1080,
quality: 85,
src: '/hero-desktop.jpg',
})
const {
props: { srcSet: mobile, ...rest },
} = getImageProps({
...common,
width: 828,
height: 1472,
quality: 80,
src: '/hero-mobile.jpg',
})
return (
<picture>
<source media="(min-width: 768px)" srcSet={desktop} />
<source media="(max-width: 767px)" srcSet={mobile} />
<img {...rest} style={{ width: '100%', height: 'auto' }} />
</picture>
);
}
3. Container Query Responsive Images
function ResponsiveCard() {
return (
<div className="container-card relative">
<div className="relative aspect-video w-full">
<Image
src="/feature.jpg"
alt="Featured content"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover"
priority={false}
/>
</div>
</div>
);
}
Dynamic Images
1. With Modern Blur Placeholder
'use client'
import { useState } from 'react'
function OptimizedProductImage({ product }) {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative">
<Image
src={product.image}
alt={product.name}
width={600}
height={400}
placeholder="blur"
blurDataURL={product.blurDataURL || generateBlurDataURL()}
className={`
w-full h-auto transition-opacity duration-500
${isLoading ? 'opacity-0' : 'opacity-100'}
`}
onLoad={() => setIsLoading(false)}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 600px"
/>
{isLoading && (
<div className="absolute inset-0 bg-gray-100 animate-pulse" />
)}
</div>
);
}
// Helper function to generate a simple blur data URL
function generateBlurDataURL() {
return `data:image/svg+xml;base64,${btoa(
'<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#f3f4f6"/></svg>'
)}`
}
2. With Intersection Observer for Performance
'use client'
import { useEffect, useRef, useState } from 'react'
function LazyOptimizedImage({ src, alt, ...props }) {
const [isVisible, setIsVisible] = useState(false)
const imgRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (imgRef.current) {
observer.observe(imgRef.current)
}
return () => observer.disconnect()
}, [])
return (
<div ref={imgRef} className="relative aspect-square bg-gray-100">
{isVisible && (
<Image
src={src}
alt={alt}
fill
className="object-cover"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
{...props}
/>
)}
</div>
)
}
Layout Patterns
1. Modern Grid Layout with Subgrid
function ImageMosaic({ images }) {
return (
<div className="grid grid-cols-12 gap-4 auto-rows-[200px]">
{images.map((image, index) => {
// Create varied sizes for visual interest
const spanClass = index % 5 === 0 ? 'col-span-6 row-span-2' :
index % 3 === 0 ? 'col-span-8' : 'col-span-4'
return (
<div key={image.id} className={`relative ${spanClass}`}>
<Image
src={image.url}
alt={image.alt}
fill
className="object-cover rounded-lg"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 60vw"
loading={index < 3 ? 'eager' : 'lazy'}
priority={index < 3}
/>
</div>
)
})}
</div>
);
}
2. Carousel with Optimized Loading
'use client'
import { useState } from 'react'
function ImageCarousel({ images }) {
const [currentIndex, setCurrentIndex] = useState(0)
return (
<div className="relative">
<div className="relative aspect-[16/9] overflow-hidden rounded-lg">
{images.map((image, index) => (
<Image
key={image.id}
src={image.url}
alt={image.alt}
fill
className={`object-cover transition-opacity duration-300 ${
index === currentIndex ? 'opacity-100' : 'opacity-0'
}`}
priority={index === 0}
loading={index < 3 ? 'eager' : 'lazy'}
sizes="(max-width: 640px) 100vw, 80vw"
/>
))}
</div>
{/* Navigation controls */}
<div className="flex justify-center mt-4 gap-2">
{images.map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
className={`w-2 h-2 rounded-full transition-colors ${
index === currentIndex ? 'bg-blue-500' : 'bg-gray-300'
}`}
/>
))}
</div>
</div>
);
}
Modern Next.js v15 Features
1. Enhanced Remote Patterns Configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// New URL object support in v15.3.0+
remotePatterns: [
new URL('https://images.unsplash.com/**'),
new URL('https://cdn.example.com/uploads/**'),
// Traditional object format still supported
{
protocol: 'https',
hostname: '**.amazonaws.com',
pathname: '/my-bucket/**',
}
],
// Enhanced format support
formats: ['image/avif', 'image/webp'],
// Quality restrictions for cost control
qualities: [25, 50, 75, 90],
},
}
module.exports = nextConfig
2. Theme-aware Images with CSS
import { getImageProps } from 'next/image'
function ThemeAwareHero() {
const common = { alt: 'Hero image', width: 1200, height: 600 }
const {
props: { srcSet: dark },
} = getImageProps({ ...common, src: '/hero-dark.jpg' })
const {
props: { srcSet: light, ...rest },
} = getImageProps({ ...common, src: '/hero-light.jpg' })
return (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={dark} />
<source media="(prefers-color-scheme: light)" srcSet={light} />
<img {...rest} className="w-full h-auto" />
</picture>
);
}
3. Advanced Background Images with CSS image-set()
import { getImageProps } from 'next/image'
function HeroSection() {
const { props: { srcSet } } = getImageProps({
alt: '',
width: 1920,
height: 1080,
src: '/hero-bg.jpg',
quality: 85
})
// Convert srcSet to CSS image-set()
const backgroundImage = srcSet
.split(', ')
.map(str => {
const [url, dpi] = str.split(' ')
return `url("${url}") ${dpi}`
})
.join(', ')
return (
<section
className="min-h-screen flex items-center justify-center"
style={{
backgroundImage: `image-set(${backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<h1 className="text-4xl font-bold text-white">Welcome</h1>
</section>
);
}
Best Practices
1. Performance-First Image Loading Strategy
function SmartImageGrid({ images, priority = false }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{images.map((image, index) => (
<div key={image.id} className="relative aspect-square">
<Image
src={image.url}
alt={image.alt}
fill
// Prioritize first 3 images, lazy load the rest
priority={priority && index < 3}
loading={priority && index < 3 ? 'eager' : 'lazy'}
className="object-cover"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
// Add decoding hint for better performance
decoding={index < 3 ? 'sync' : 'async'}
/>
</div>
))}
</div>
);
}
2. Error Boundary for Images
'use client'
import { ErrorBoundary } from 'react-error-boundary'
function ImageErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="flex flex-col items-center justify-center p-8 bg-gray-100 rounded-lg">
<div className="text-gray-400 mb-2">📷</div>
<p className="text-sm text-gray-600 mb-4">Failed to load image</p>
<button
onClick={resetErrorBoundary}
className="px-4 py-2 bg-blue-500 text-white rounded text-sm hover:bg-blue-600"
>
Try again
</button>
</div>
)
}
function SafeImageWrapper({ children }) {
return (
<ErrorBoundary FallbackComponent={ImageErrorFallback}>
{children}
</ErrorBoundary>
)
}
3. Image Component with Modern Loading States
'use client'
import { useState } from 'react'
function ModernImage({ src, alt, ...props }) {
const [loadingState, setLoadingState] = useState('loading')
return (
<div className="relative">
<Image
src={src}
alt={alt}
onLoadStart={() => setLoadingState('loading')}
onLoad={() => setLoadingState('loaded')}
onError={() => setLoadingState('error')}
className={`transition-opacity duration-300 ${
loadingState === 'loaded' ? 'opacity-100' : 'opacity-0'
}`}
{...props}
/>
{loadingState === 'loading' && (
<div className="absolute inset-0 bg-gradient-to-r from-gray-200 via-gray-100 to-gray-200 animate-pulse" />
)}
{loadingState === 'error' && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100 text-gray-500">
<span>Failed to load</span>
</div>
)}
</div>
)
}
Key Takeaways for Next.js v15
-
Enhanced Configuration:
- URL object support in
remotePatterns
for cleaner configuration - Quality restrictions to control optimization costs
- Better format support with AVIF prioritization
- URL object support in
-
Performance Optimizations:
- Use
priority
for above-the-fold images only - Implement proper
sizes
attribute for responsive images - Leverage
getImageProps
for advanced use cases - Use
decoding
hints for better performance
- Use
-
Modern Patterns:
- CSS
image-set()
for background images - Picture element for art direction
- Container queries for truly responsive design
- Theme-aware image loading
- CSS
-
Developer Experience:
- Better error handling with fallbacks
- Loading state management
- TypeScript support out of the box
- Improved debugging with better error messages
Conclusion
The Next.js Image component in v15 continues to evolve with better performance, enhanced configuration options, and modern web standards support. By following these patterns and best practices, you can ensure your images are performant, accessible, and provide an excellent user experience across all devices.
Remember to always consider your specific use case, test performance on real devices, and monitor your Core Web Vitals to ensure optimal results.
This guide reflects Next.js v15 best practices and features as of 2024.