Skip to content
Codeloom
Next.js

Next.js Image Optimization Strategies with next/image

Master the next/image component with advanced optimization strategies including responsive images, blur placeholders, CDN configuration, and performance tips.

·6 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • How the next/image component optimizes images automatically
  • Configuring responsive sizes and srcset for different viewports
  • Using blur placeholders and priority loading for LCP images
  • Setting up external image domains and custom loaders
  • Advanced patterns for art direction and CDN integration

Prerequisites

  • Working knowledge of [Next.js basics](/blog/nextjs-install-and-first-app)
  • Understanding of [Core Web Vitals](/blog/web-performance-core-web-vitals-guide)

The next/image component does more than resize images. It generates multiple sizes on demand, serves modern formats like WebP and AVIF, lazy-loads by default, and prevents Cumulative Layout Shift by reserving space. Understanding how to configure it properly is the difference between a fast site and one that serves 2 MB hero images to mobile users.

The basics of next/image

import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Mountain landscape at sunrise"
      width={1200}
      height={630}
    />
  );
}

This single component handles:

  • Format conversion: Serves WebP or AVIF depending on browser support
  • Resizing: Generates optimized versions at the requested dimensions
  • Lazy loading: Only loads the image when it enters the viewport
  • Layout stability: Reserves the correct aspect ratio to prevent CLS

Responsive images with sizes

The sizes prop tells the browser which image width to download based on the viewport:

<Image
  src="/product.jpg"
  alt="Product photo"
  width={800}
  height={600}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>

This generates a srcset with multiple resolutions. Without sizes, the browser defaults to downloading the full-width image regardless of how much space the image actually occupies. For a three-column grid on desktop where each image is only 33% of the viewport, you would waste bandwidth downloading images three times larger than needed.

Common sizes patterns:

// Full-width hero
sizes="100vw"

// Two-column grid
sizes="(max-width: 768px) 100vw, 50vw"

// Sidebar thumbnail
sizes="(max-width: 768px) 100vw, 200px"

// Card in responsive grid
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"

Fill mode for unknown dimensions

When the image dimensions are not known at build time, use the fill prop:

<div className="relative w-full aspect-video">
  <Image
    src={post.coverImage}
    alt={post.title}
    fill
    className="object-cover rounded-lg"
    sizes="(max-width: 768px) 100vw, 720px"
  />
</div>

The fill prop makes the image fill its parent container. The parent must have position: relative (or absolute/fixed). Always pair fill with an explicit sizes prop, otherwise the browser assumes the image is full-width.

Priority loading for LCP images

The Largest Contentful Paint (LCP) image should load as fast as possible. Add the priority prop to disable lazy loading and preload the image:

<Image
  src="/hero-banner.jpg"
  alt="Summer collection"
  width={1920}
  height={1080}
  priority
  sizes="100vw"
/>

Only use priority for above-the-fold images that are the LCP element. Adding it to every image defeats the purpose and slows down the initial load.

Blur placeholders

Show a blurred preview while the full image loads:

// For static imports, the blur hash is generated automatically
import heroImg from '@/public/hero.jpg';

<Image
  src={heroImg}
  alt="Hero"
  placeholder="blur"
/>

For dynamic images, provide a base64-encoded blur data URL:

<Image
  src={product.imageUrl}
  alt={product.name}
  width={400}
  height={300}
  placeholder="blur"
  blurDataURL={product.blurHash}
/>

Generate blur data URLs at upload time using a library like plaiceholder:

import { getPlaiceholder } from 'plaiceholder';

async function getBlurDataUrl(imageUrl: string) {
  const res = await fetch(imageUrl);
  const buffer = Buffer.from(await res.arrayBuffer());
  const { base64 } = await getPlaiceholder(buffer, { size: 10 });
  return base64;
}

Configuring external image domains

To use images from external URLs, configure allowed domains in next.config.ts:

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

const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      {
        protocol: 'https',
        hostname: '*.amazonaws.com',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/uploads/**',
      },
    ],
  },
};

export default config;

Use remotePatterns instead of the deprecated domains array. Patterns support wildcards and path restrictions for better security.

Custom image loaders

Replace the default Next.js image optimization with a third-party CDN:

// next.config.ts
const config: NextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.ts',
  },
};
// lib/image-loader.ts
interface ImageLoaderParams {
  src: string;
  width: number;
  quality?: number;
}

export default function cloudinaryLoader({
  src,
  width,
  quality,
}: ImageLoaderParams): string {
  const params = [
    `f_auto`,
    `c_limit`,
    `w_${width}`,
    `q_${quality || 'auto'}`,
  ];
  return `https://res.cloudinary.com/your-cloud/image/upload/${params.join(',')}${src}`;
}

This redirects all next/image requests through Cloudinary’s transformation pipeline, giving you access to advanced features like face detection cropping and automatic format selection.

Art direction with the picture element

For cases where you need completely different images at different breakpoints (not just different sizes of the same image), use the native <picture> element:

export function ResponsiveHero() {
  return (
    <picture>
      <source
        media="(max-width: 640px)"
        srcSet="/hero-mobile.webp"
        type="image/webp"
      />
      <source
        media="(max-width: 1024px)"
        srcSet="/hero-tablet.webp"
        type="image/webp"
      />
      <Image
        src="/hero-desktop.jpg"
        alt="Hero banner"
        width={1920}
        height={600}
        priority
      />
    </picture>
  );
}

Performance configuration

Fine-tune the image optimization pipeline:

// next.config.ts
const config: NextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
    qualities: [25, 50, 75],
  },
};
  • formats: AVIF is smaller than WebP but slower to encode. List it first so browsers that support it get the better format.
  • deviceSizes: The widths used for 100vw images. Remove sizes you do not need to reduce the number of generated variants.
  • imageSizes: Widths for images with explicit sizes values smaller than the smallest device size.
  • minimumCacheTTL: How long optimized images are cached. Increase this for images that rarely change.

Common mistakes

Missing sizes prop. Without it, the browser downloads the largest image variant. This is the single most common performance issue with next/image.

Using fill without a sized parent. The image collapses to zero height or overflows because it has no reference for its dimensions.

Adding priority to too many images. Only the LCP image needs it. Preloading multiple images competes for bandwidth and slows everything down.

Not setting alt text. Beyond accessibility, missing alt text hurts SEO. Every image should have descriptive alt text or an empty alt="" for decorative images.

The next/image component handles most optimization automatically, but configuring sizes, priority, and placeholder correctly transforms a good setup into a great one. Measure your changes with Lighthouse and the Chrome DevTools network tab to verify that the right image sizes are being served at each breakpoint.