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.

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"
    />
  )
}
What next/image does automatically: Converts to WebP/AVIF, generates responsive srcset, adds width/height to prevent CLS, lazy-loads by default, serves optimized images from /_next/image endpoint, caches on CDN.

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],
  },
}
AVIF vs WebP: AVIF is 20–50% smaller than WebP at the same quality. However, AVIF encoding is slow (adds build time) and isn't supported in some older browsers. WebP has near-universal support. Next.js serves AVIF to supporting browsers and falls back to WebP automatically.

Core Web Vitals Impact

Images affect three Core Web Vitals:

  • LCP (Largest Contentful Paint) — Hero images are usually the LCP element. Use priority + correct sizes to minimize LCP
  • CLS (Cumulative Layout Shift) — Images without explicit width/height shift layout on load. next/image always 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>
  )
}