X Xerobit

Next.js Image Optimization — next/image Component Guide

The Next.js Image component automatically optimizes images: serving WebP/AVIF, lazy loading, preventing layout shift with explicit dimensions, and resizing for responsive...

Mian Ali Khalid · · 5 min read
Use the tool
Image Compressor
Compress JPEG, PNG, and WebP images in your browser. Adjustable quality, batch mode. Files never leave your device.
Open Image Compressor →

The next/image component wraps <img> with automatic format conversion (WebP/AVIF), lazy loading, and layout-shift prevention. Using it correctly eliminates most image-related Lighthouse warnings.

Compress images before importing with the Image Compressor.

Basic usage

import Image from 'next/image';

// Local image (width/height inferred from file):
import heroImage from '@/public/hero.jpg';

export default function Hero() {
  return (
    <Image
      src={heroImage}
      alt="Hero image"
      priority  // Above-the-fold images: remove lazy loading
    />
  );
}

// Remote image (width/height required):
export default function Avatar({ user }) {
  return (
    <Image
      src={user.avatarUrl}
      alt={user.name}
      width={64}
      height={64}
    />
  );
}

width and height props

These don’t set the rendered size — they set the aspect ratio and prevent layout shift:

// Rendered at CSS width/height, but aspect ratio from props:
<Image
  src="/photo.jpg"
  width={800}
  height={600}
  alt="Photo"
  style={{ width: '100%', height: 'auto' }}  // Fill container
/>

// Or use Tailwind:
<Image
  src="/photo.jpg"
  width={800}
  height={600}
  alt="Photo"
  className="w-full h-auto"
/>

Without width/height, the browser doesn’t know the image size until it loads, causing Cumulative Layout Shift (CLS).

fill: responsive images that fill a container

// fill mode: image fills parent — parent must have position: relative
<div className="relative w-full h-64">
  <Image
    src="/landscape.jpg"
    alt="Landscape"
    fill
    sizes="100vw"
    className="object-cover"  // or object-contain
  />
</div>

// Product card with fixed aspect ratio:
<div className="relative aspect-square w-48">
  <Image
    src={product.image}
    alt={product.name}
    fill
    sizes="192px"
    className="object-contain p-4"
  />
</div>

sizes: tell the browser which size to download

sizes prevents downloading a 1200px image for a 300px thumbnail:

// Without sizes: browser might download the largest image
<Image src="/photo.jpg" fill alt="Photo" />

// With sizes: browser calculates which srcset entry to download
<Image
  src="/photo.jpg"
  fill
  alt="Photo"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
// At 400px viewport: downloads the ~400px version
// At 1400px viewport in 3-column grid: downloads the ~467px version

priority: above-the-fold images

// Hero image: no lazy loading, starts fetching immediately
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
/>

// Rule: mark priority on the Largest Contentful Paint (LCP) image
// Without priority: lazy loading defers LCP image = worse performance score

Only the first 1-2 above-fold images need priority. Using it on every image defeats lazy loading.

Remote image domains

Next.js blocks remote images by default to prevent abuse:

// next.config.js (Next.js 13+ with remotePatterns):
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com',  // Wildcard subdomain
      },
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        pathname: '/my-bucket/**',       // Restrict to specific path
      },
    ],
  },
};

Custom loader for CDNs

// next.config.js: Cloudinary loader
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/cloudinary-loader.js',
  },
};
// lib/cloudinary-loader.js
export default function cloudinaryLoader({ src, width, quality }) {
  const params = [
    'f_auto',           // Auto format (AVIF/WebP)
    'c_limit',          // Don't upscale
    `w_${width}`,
    `q_${quality || 75}`,
  ];
  return `https://res.cloudinary.com/my-cloud/image/upload/${params.join(',')}/${src}`;
}
// Now Image component uses Cloudinary URLs:
<Image
  src="products/shirt.jpg"  // Just the path, no domain
  width={400}
  height={400}
  alt="Shirt"
/>
// → https://res.cloudinary.com/my-cloud/image/upload/f_auto,c_limit,w_400,q_75/products/shirt.jpg

Placeholder blur (low-quality image placeholder)

import Image from 'next/image';
import heroImage from '@/public/hero.jpg';

// Local images: blurDataURL generated automatically
<Image
  src={heroImage}
  alt="Hero"
  placeholder="blur"
/>

// Remote images: provide blurDataURL manually (base64 tiny placeholder)
// Generate with: https://blurha.sh or plaiceholder npm package
<Image
  src="https://example.com/photo.jpg"
  width={800}
  height={600}
  alt="Photo"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/..."
/>

Lighthouse checklist

IssueSolution
Missing width/heightAdd explicit width/height props
LCP image lazy loadedAdd priority prop
Oversized imageAdd sizes prop matching CSS layout
No WebP/AVIFUse next/image (automatic)
Layout shiftAdd width/height or use fill with sized parent

Related posts

Related tool

Image Compressor

Compress JPEG, PNG, and WebP images in your browser. Adjustable quality, batch mode. Files never leave your device.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.