All insights
Engineering6 min read

Next.js Image Optimization: A Complete Guide to next/image

Images are the most common cause of poor Core Web Vitals. next/image solves most image performance problems automatically — if you use it correctly.

NC

Nextcraft Engineering Team

Why next/image Exists

Raw <img> tags in web applications cause predictable problems:

  • No automatic format conversion (no WebP/AVIF for older browsers serving JPEG)
  • No responsive sizing (serving a 2000px image to a 400px mobile screen)
  • No lazy loading by default (loading off-screen images on page load)
  • No dimension reservation (layout shift when images load)
  • No blur-up placeholder (jarring appearance when images load)

next/image solves all of these automatically. The tradeoff: you provide dimensions or a fill prop, and the component handles everything else.

Basic Usage

code
import Image from 'next/image';

// Fixed dimensions — you know the exact display size
<Image
  src="/hero.jpg"
  alt="Hero image describing what's shown"
  width={1200}
  height={630}
/>

// Fill mode — image fills a positioned container
<div className="relative h-64 w-full">
  <Image
    src="/cover.jpg"
    alt="Cover image"
    fill
    className="object-cover"
  />
</div>

The priority Prop: Critical for LCP

The largest above-the-fold image is your LCP element. It should never be lazy-loaded — it should preload:

code
// Hero image — above the fold, mark as priority
<Image
  src="/hero.jpg"
  alt="Agency hero"
  width={1200}
  height={600}
  priority  // Adds preload link, disables lazy loading
/>

Use priority on:

  • The hero image on any page
  • The first image in a list that's always visible on load
  • Any image that's likely to be the LCP element

Don't use priority on everything — it defeats the purpose by preloading images that are below the fold.

Remote Images: The remotePatterns Config

For images from external domains (a CMS, a CDN, user avatars), configure allowed domains:

code
// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.contentful.com',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com',  // Wildcard subdomain
      },
      {
        protocol: 'https',
        hostname: 'avatars.githubusercontent.com',
        pathname: '/u/**',  // Restrict to specific path
      },
    ],
  },
};

Responsive Images with sizes

The sizes prop tells the browser which size image to download based on viewport. Without it, the browser guesses — often poorly:

code
// Image that's full width on mobile, 50% on tablet, 33% on desktop
<Image
  src="/product.jpg"
  alt="Product photo"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

The browser downloads only what it needs. On a 400px mobile screen requesting a 33vw image on a 1440px design, without sizes it downloads the 480px image; with correct sizes it downloads the 400px image. Significant bandwidth savings on mobile.

Blur Placeholders

For images that take time to load (large hero images, high-res photos), a blur placeholder prevents the jarring white flash:

code
// Static blur placeholder (generated at build time)
import heroImage from './hero.jpg'; // Import as module

<Image
  src={heroImage}
  alt="Hero"
  placeholder="blur"  // Uses auto-generated blur data URL
  priority
/>

For dynamic images (from a CMS), generate the blur data URL at build/request time:

code
import { getPlaiceholder } from 'plaiceholder';

async function getImageWithBlur(src: string) {
  const buffer = await fetch(src).then(r => r.arrayBuffer());
  const { base64 } = await getPlaiceholder(Buffer.from(buffer));
  return base64;
}

// In a Server Component
const blurDataURL = await getImageWithBlur(post.coverImage);

<Image
  src={post.coverImage}
  alt={post.title}
  width={1200}
  height={630}
  placeholder="blur"
  blurDataURL={blurDataURL}
/>

Format and Quality

Next.js automatically serves WebP to browsers that support it, and AVIF where supported. You can configure quality:

code
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  quality={85}  // Default is 75. Range: 1-100
/>

85 is a good balance between quality and file size for photography. UI screenshots can go lower (70–75). Icons and graphics should use SVG instead.

Custom Loaders for CDN Integration

If you're using Cloudinary, Imgix, or another image CDN with its own transformation API:

code
// lib/imageLoader.ts
import type { ImageLoader } from 'next/image';

const cloudinaryLoader: ImageLoader = ({ src, width, quality }) => {
  return `https://res.cloudinary.com/yourcloud/image/upload/w_${width},q_${quality ?? 75},f_auto/${src}`;
};

// Usage
<Image
  src="my-image.jpg"  // Just the public ID
  alt="Image"
  width={800}
  height={600}
  loader={cloudinaryLoader}
/>

Custom loaders let you leverage your CDN's transformation pipeline while keeping the next/image component's other benefits (lazy loading, responsive srcset, placeholder).

Common Mistakes

Nesting <Image> inside <a> without wrapping properly:

code
// Wrong
<a href="/page"><Image src="..." alt="..." width={100} height={100} /></a>

// Correct — Link wraps Image directly
<Link href="/page"><Image src="..." alt="..." width={100} height={100} /></Link>

Using fill without a positioned parent:

code
// Wrong — fill needs a positioned parent
<Image src="..." alt="..." fill />

// Correct
<div className="relative h-48">
  <Image src="..." alt="..." fill className="object-cover" />
</div>

Empty or generic alt text:

code
// Wrong
<Image src="..." alt="" />
<Image src="..." alt="image" />

// Correct — describe what the image shows
<Image src="..." alt="Screenshot of the dashboard showing monthly revenue trends" />

Descriptive alt text is both an accessibility requirement and an SEO signal for image search.

Stay Informed.

Join 1,200+ founders and engineers receiving our monthly deep dives on product engineering, design, and growth.

Insights once a month. No spam. Unsubscribe anytime.