Chapter 17

Image Component: Automatic Image Optimization

Why Not Just Use a Plain <img> Tag

The native HTML <img> tag has several inherent weaknesses from a modern web performance perspective:

Format problem: Browsers natively support WebP and AVIF, two highly efficient formats, but most image servers still store files as JPEG or PNG. AVIF typically achieves over 50% better compression than JPEG at equivalent quality. Manually converting every image is impractical, and format selection requires detecting browser support at request time.

Size problem: Mobile devices have screens typically 375โ€“414px wide, yet servers often serve 2000px-wide originals. Users download 10 times more data than they actually need.

CLS (Cumulative Layout Shift) problem: An <img> tag without width and height attributes occupies zero height until the image loads, then pushes the layout down โ€” causing the page to "jump." This directly harms the CLS metric in Core Web Vitals.

Lazy loading problem: Images below the fold should not be requested on page load, but <img> tags load all images immediately by default.

Next.js's Image component from next/image solves all of these problems, mostly out of the box with no configuration required.

Basic Usage and Automatic Optimization

import Image from 'next/image'

export default function ProductCard() {
  return (
    <Image
      src="/products/shoe.jpg"
      alt="Nike Air Max Sneakers"
      width={800}
      height={600}
      className="rounded-lg"
    />
  )
}

These few lines trigger the following optimizations:

  1. Format conversion: Next.js detects browser support and serves AVIF first, then WebP, then falls back to the original format.
  2. Resizing: A version sized to match the actual rendered dimensions is generated, avoiding large unnecessary downloads.
  3. Lazy loading: Enabled by default โ€” images outside the viewport are not requested immediately.
  4. CLS prevention: The aspect ratio is computed from width and height, reserving space before the image loads.

Optimization is on-demand: the first request for a given size triggers real-time processing and caching. Subsequent requests are served from the cache.

fill Mode: Container-Driven Image Sizing

When an image needs to fill its parent container, use the fill prop instead of fixed width/height:

import Image from 'next/image'

export function CoverImage({ src, alt }: { src: string; alt: string }) {
  return (
    <div className="relative w-full aspect-video">
      <Image
        src={src}
        alt={alt}
        fill
        className="object-cover"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
    </div>
  )
}

A few requirements when using fill:

object-cover makes the image cover the entire container with cropping; object-contain shows the full image with letterboxing.

The priority Prop: The Critical Optimization for LCP Images

Lazy loading is valuable, but the largest content element on the first screen (the LCP element) should not be lazy-loaded โ€” lazy loading actually delays LCP. The priority prop tells Next.js this image is high priority:

// Hero image on the homepage: always add priority
<Image
  src="/hero.jpg"
  alt="Site hero visual"
  width={1920}
  height={1080}
  priority
/>

priority does two things:

  1. Disables lazy loading so the image starts downloading immediately
  2. Adds a <link rel="preload"> hint in the HTML <head>, letting the browser discover and start downloading the image sooner

Measured impact: In production projects, adding priority to the above-the-fold hero image typically improves LCP by 200โ€“500ms. Lighthouse will warn about "Image elements do not have explicit width and height" and "Largest Contentful Paint image was not preloaded" โ€” both of these are fixed by using next/image correctly.

When not to use priority: the vast majority of images are not above the fold and should not receive priority. Adding it to every image defeats the purpose and increases initial load burden. As a rule of thumb, most pages have only 1โ€“2 images that warrant priority.

The sizes Attribute: Precise Control for Responsive Images

sizes is the most commonly overlooked attribute in next/image, yet it has the largest performance impact. It tells the browser (and Next.js's image optimizer) how wide the image will be on screen at different viewport widths:

// Responsive layout: full width on mobile, half on tablet, one-third on desktop
<Image
  src="/product.jpg"
  alt="Product image"
  fill
  sizes="(max-width: 640px) 100vw,
         (max-width: 1024px) 50vw,
         33vw"
/>

Without the sizes attribute, the browser assumes the image fills 100vw and requests a full-width version. For an image that only occupies 33% of the desktop viewport, that means downloading 3 times more data than needed.

Next.js generates a srcset based on sizes and its built-in breakpoints:

<!-- HTML generated by Next.js -->
<img
  srcset="
    /_next/image?url=%2Fproduct.jpg&w=640&q=75  640w,
    /_next/image?url=%2Fproduct.jpg&w=828&q=75  828w,
    /_next/image?url=%2Fproduct.jpg&w=1080&q=75 1080w
  "
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  ...
/>

The browser uses the current viewport width and sizes to calculate the needed image width, then selects the best match from srcset.

Remote Image Domain Configuration

When using images from external domains, you must explicitly authorize them in next.config.ts to prevent abuse of the image optimization service:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        port: '',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com',  // Wildcard matches subdomains
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
}

export default nextConfig

Images from domains not listed in remotePatterns return a 400 error. ** matches any subdomain; * matches a single path segment.

Custom Loaders: Integrating with a CDN

If you use a professional image CDN like Cloudinary, Imgix, or Akamai, the loader prop lets you take full control of how image URLs are generated:

// lib/imageLoader.ts
import type { ImageLoaderProps } from 'next/image'

export function cloudinaryLoader({ src, width, quality }: ImageLoaderProps) {
  const params = [
    'f_auto',              // Automatic format selection (AVIF/WebP/JPEG)
    'c_limit',             // Cap size at original
    `w_${width}`,          // Width
    `q_${quality ?? 75}`,  // Quality
  ].join(',')

  return `https://res.cloudinary.com/mycloud/image/upload/${params}/${src}`
}
// Using the custom loader
import Image from 'next/image'
import { cloudinaryLoader } from '@/lib/imageLoader'

export function CloudinaryImage({
  publicId,
  alt,
  width,
  height,
}: {
  publicId: string
  alt: string
  width: number
  height: number
}) {
  return (
    <Image
      loader={cloudinaryLoader}
      src={publicId}
      alt={alt}
      width={width}
      height={height}
    />
  )
}

A custom loader takes complete ownership of URL generation โ€” Next.js's built-in optimizer is bypassed entirely, and all format conversion and resizing is handled by the CDN. This is the better choice when you already have a professional image processing service, avoiding double optimization.

You can also configure the loader globally in next.config.ts:

// next.config.ts
const nextConfig: NextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './lib/imageLoader.ts',
  },
}

Blur Placeholders: Improving the Loading Experience

placeholder="blur" shows a blurry preview before the image loads, avoiding the white empty area:

// Local images: Next.js auto-generates blurDataURL at build time
import heroImage from '@/public/hero.jpg'

<Image
  src={heroImage}
  alt="Hero image"
  placeholder="blur"
  priority
/>

// Remote images: you must provide blurDataURL manually
// Typically a base64-encoded 10x10 pixel image
<Image
  src="https://cdn.example.com/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
/>

To generate blurDataURL for remote images at build time, you can use the plaiceholder library:

// scripts/generateBlurData.ts
import { getPlaiceholder } from 'plaiceholder'

export async function getBlurDataUrl(url: string) {
  const { base64 } = await getPlaiceholder(url)
  return base64
}

The Real Impact of Image Optimization on LCP

Consider a realistic scenario: an e-commerce homepage with a 2.4MB JPEG hero image (3000x2000 pixels).

Before optimization (native <img>):

After optimization (next/image + priority + sizes):

The gap comes from three compounding effects: more efficient formats (AVIF compression), appropriately sized images (no longer downloading a 3000px-wide image for a 375px mobile screen), and preloading (priority lets the browser discover the image earlier).

Common Mistakes and Best Practices

Mistake 1: Adding priority to every image. The priority signal loses meaning and the initial page load slows down. Reserve it for LCP images only.

Mistake 2: Omitting sizes with fill mode. Next.js defaults to generating images at 100vw even when the image only occupies one-third of the container width.

Mistake 3: Not handling null when interpolating dynamic URLs into src.

// Wrong
<Image src={user.avatar} alt={user.name} ... />

// Correct: provide a fallback
<Image
  src={user.avatar ?? '/default-avatar.png'}
  alt={user.name}
  ...
/>

Mistake 4: Using max-width: 100% in CSS to override Next.js inline styles. next/image sets inline width and height styles; overriding them with !important in CSS breaks layout calculation.

Summary

next/image is one of the few optimizations that can deliver significant performance improvements โ€” often Lighthouse scores jumping 20-40 points โ€” purely by replacing a single HTML tag, with no change to product functionality. Understanding the relationship between priority and lazy loading, the effect of sizes on srcset generation, and the conditions for using fill mode are the keys to unlocking the full potential of next/image. For projects that use external image CDNs, a custom loader provides an additional layer of control over the entire image processing pipeline.

Rate this chapter
4.9  / 5  (13 ratings)

๐Ÿ’ฌ Comments