Next.js Image Optimization and Core Web Vitals (2026)
Images are the single largest contributor to poor Core Web Vitals scores. The next/image component automatically handles WebP/AVIF conversion, responsive srcsets, lazy loading, CLS prevention and blur placeholders — turning image optimization from a complex build step into a declarative API. Used correctly, it can cut your LCP by 50% or more.
Table of Contents
next/image Basics
import Image from 'next/image'
// Local image — Next.js knows width/height from import
import heroImage from '@/public/hero.jpg'
function Hero() {
return (
<Image
src={heroImage}
alt="Hero banner showing our product"
// width and height auto-inferred from import
priority // Above the fold — don't lazy load
/>
)
}
// Remote image — must specify width and height
function Avatar({ user }) {
return (
<Image
src={user.avatarUrl}
alt={`${user.name}'s avatar`}
width={64}
height={64}
className="rounded-full"
/>
)
}
Above-the-Fold: priority Prop
The priority prop disables lazy loading and adds <link rel="preload"> — critical for hero images that affect LCP:
// app/page.tsx — hero image must be priority
export default function HomePage() {
return (
<section>
<Image
src="/hero.jpg"
alt="Platform overview"
width={1200}
height={600}
priority // Preloads — no lazy load
quality={90}
/>
</section>
)
}
// Only ONE image per page should have priority — the one that
// will be the Largest Contentful Paint element.
// All other images default to lazy loading.
Responsive Images: sizes Prop
The sizes prop tells the browser how wide the image will be at different viewport widths, so it can download the right size:
// Full-width hero
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="100vw"
priority
/>
// Sidebar image (1/3 of viewport on desktop, full on mobile)
<Image
src={post.image}
alt={post.title}
width={400}
height={300}
sizes="(max-width: 768px) 100vw, 33vw"
/>
// Card in a 3-column grid
<Image
src={product.image}
alt={product.name}
width={400}
height={400}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
// Without sizes: Next.js generates srcset assuming image is 100vw
// — browsers download full-width images for all grid cards (wasteful)
// With correct sizes: browsers download appropriately sized images
Fill Layout
Use fill when you don't know the image dimensions or want it to fill a container:
// Container must have position: relative and explicit dimensions
function ProductCard({ product }) {
return (
<div className="relative h-64 w-full overflow-hidden rounded-xl">
<Image
src={product.image}
alt={product.name}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover" // or object-contain
style={{ objectPosition: 'center top' }}
/>
</div>
)
}
// Background image pattern
function PageHero() {
return (
<div className="relative h-[500px]">
<Image
src="/bg.jpg"
alt="" // Decorative — empty alt
fill
sizes="100vw"
className="object-cover -z-10"
priority
/>
<div className="relative z-10 flex h-full items-center justify-center">
<h1 className="text-5xl font-bold text-white">Welcome</h1>
</div>
</div>
)
}
Remote Images
Whitelist remote image domains in next.config.ts:
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '**.cloudinary.com', // Wildcard subdomain
},
{
protocol: 'https',
hostname: 'cdn.example.com',
port: '',
pathname: '/uploads/**', // Restrict to path
},
],
// Optional: set quality default
qualities: [75, 90],
},
}
export default nextConfig
// Now remote images work with next/image
<Image
src="https://images.unsplash.com/photo-12345?w=800"
alt="Mountain landscape"
width={800}
height={600}
/>
Blur Placeholder
// Local images — blurDataURL auto-generated from the import
import profilePic from '@/public/profile.jpg'
<Image
src={profilePic}
alt="Profile"
placeholder="blur" // Shows blurred version while loading
/>
// Remote images — provide blurDataURL manually
// Generate with: npx plaiceholder https://example.com/image.jpg
<Image
src="https://example.com/large-photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
/>
// Generate blurDataURL at build/request time with plaiceholder
import { getPlaiceholder } from 'plaiceholder'
async function BlogPost({ params }) {
const post = await fetchPost(params.slug)
const { base64 } = await getPlaiceholder(post.heroImage)
return (
<Image
src={post.heroImage}
alt={post.title}
width={1200}
height={630}
placeholder="blur"
blurDataURL={base64}
/>
)
}
Formats: WebP and AVIF
// next.config.ts — configure output formats
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // AVIF tried first, WebP fallback
// Default: ['image/webp'] only
minimumCacheTTL: 60, // Cache optimized images for 60 seconds min
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
Core Web Vitals Impact
Images affect three Core Web Vitals:
- LCP (Largest Contentful Paint) — Hero images are usually the LCP element. Use
priority+ correctsizesto minimize LCP - CLS (Cumulative Layout Shift) — Images without explicit width/height shift layout on load.
next/imagealways reserves space, eliminating this - INP (Interaction to Next Paint) — Images themselves don't affect INP but heavy image decoding on the main thread can. Use
decoding="async"(default in next/image)
Common Patterns
Avatar with fallback:
function UserAvatar({ user, size = 40 }) {
const [imgError, setImgError] = useState(false)
if (imgError || !user.avatarUrl) {
return (
<div
className="flex items-center justify-center rounded-full bg-indigo-500 text-white font-bold"
style={{ width: size, height: size, fontSize: size * 0.4 }}
>
{user.name[0].toUpperCase()}
</div>
)
}
return (
<Image
src={user.avatarUrl}
alt={user.name}
width={size}
height={size}
className="rounded-full object-cover"
onError={() => setImgError(true)}
/>
)
}
Image gallery with masonry:
function Gallery({ images }) {
return (
<div className="columns-2 md:columns-3 gap-4">
{images.map((img, i) => (
<div key={img.id} className="mb-4 break-inside-avoid">
<Image
src={img.url}
alt={img.caption}
width={img.width}
height={img.height}
sizes="(max-width: 768px) 50vw, 33vw"
className="w-full h-auto rounded-lg"
priority={i < 2} // Priority for first 2 images
/>
</div>
))}
</div>
)
}