| | |

Next.js 12 ▲ Image Optimization — next/image component & responsive sizing

⏱️ Estimated read: 12 min | |

The next/image component is a drop-in replacement for the HTML <img> element that adds automatic optimization, responsive sizing, and layout stability. It does not just render an image tag; it generates a srcset with multiple width variants, selects the best format the browser supports (WebP or AVIF), and prevents the layout shift that occurs when an image loads without known dimensions . The component is the default way to handle images in Next.js, and the decisions you make about sizes, priority, and the config file determine whether it actually improves performance or silently falls back to serving oversized originals.

This chapter covers the component’s local and remote image handling, the width/height and fill modes, the sizes attribute that drives responsive selection, the priority flag for LCP images, and the next.config.ts settings that control remote patterns, device sizes, and format negotiation. It also covers the Next.js 16 changes that tightened defaults and removed the images.domains option.

Key point: The sizes attribute is not optional for responsive images. Without it, the browser assumes 100vw and downloads the largest variant, defeating the optimization. The priority flag should be set on exactly one image per page: the LCP image.


Why next/image exists

The layout shift problem. An <img> tag without explicit width and height attributes has no intrinsic size until the image loads. The browser reserves zero space, then reflows the page when the image arrives. This is Cumulative Layout Shift (CLS), and it is a measurable user experience problem. next/image requires either width/height or fill, which reserves space before the image loads and eliminates the shift .

The oversized image problem. A 2000px-wide image displayed in a 400px container wastes bandwidth. Without a srcset, the browser downloads the 2000px original and scales it down. next/image generates a set of width variants and a sizes hint, so the browser downloads the smallest variant that fits the layout .

The format problem. JPEG and PNG are universal but not optimal. WebP and AVIF provide better compression at the same visual quality. next/image checks the browser’s Accept header and serves the best supported format automatically. If the application sits behind a proxy or CDN that does not forward the Accept header, the format negotiation fails silently and the original format is served .


a. Local images and static imports

A local image can be referenced by its path under public/, or imported directly. The static import is preferred because Next.js reads the file at build time and determines the intrinsic width, height, and blurDataURL automatically .

// Path reference: width and height required
<Image src="/profile.png" alt="Profile" width={500} height={500} />

// Static import: dimensions inferred automatically
import ProfileImage from './profile.png'

<Image src={ProfileImage} alt="Profile" placeholder="blur" />

The static import produces an object with src, width, height, and blurDataURL. The placeholder="blur" prop uses the generated blurDataURL to show a low-quality preview while the full image loads. For dynamic imports inside a Server Component, the import() expression can load the image and pass the resulting object to <Image> .

b. Remote images and the fill mode

A remote image requires width and height props because Next.js cannot inspect the file at build time. The dimensions establish the aspect ratio and reserve layout space. Alternatively, the fill prop makes the image expand to the size of its parent, which must have position: relative and a defined size .

// Remote image with explicit dimensions
<Image
  src="https://s3.amazonaws.com/my-bucket/profile.png"
  alt="Profile"
  width={500}
  height={500}
/>

// Fill mode: parent must be positioned
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    sizes="(max-width: 768px) 100vw, 50vw"
    style={{ objectFit: 'cover' }}
  />
</div>

Remote images must be allow-listed in next.config.ts using remotePatterns. The pattern specifies the protocol, hostname, port, and pathname. A broad pattern like hostname: '**' allows any remote image and is a security risk; the pattern should be as specific as the actual sources .

// next.config.ts
const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

c. Responsive sizing with sizes

The sizes prop tells the browser how much viewport width the image will occupy at different breakpoints. Without it, the browser assumes the image spans the full viewport (100vw), which is rarely correct for grid layouts or sidebar images. The browser uses sizes to select the smallest srcset variant that satisfies the layout .

// A card image that is full width on mobile and one-third on desktop
<Image
  src="/card.jpg"
  alt="Card"
  fill
  sizes="(max-width: 768px) 100vw, 33vw"
/>

The sizes string is CSS media query syntax. (max-width: 768px) 100vw means “below 768px, the image is 100% of the viewport width.” 33vw means “above 768px, the image is one-third of the viewport width.” The browser multiplies the viewport width by the percentage to determine the required image width, then picks the closest variant from the srcset that next/image generated .

The next.config.ts deviceSizes and imageSizes arrays control the widths that appear in the srcset. The defaults are extensive (~16 variants), which multiplies the number of image transformations and, on Vercel, the transform quota. Tightening these arrays to the actual breakpoints the design uses reduces the matrix .

const config: NextConfig = {
  images: {
    deviceSizes: [640, 750, 1080, 1920],
    imageSizes: [16, 32, 64, 128],
    formats: ['image/avif', 'image/webp'],
  },
}

d. Priority and the LCP image

The priority prop disables lazy loading and adds a preload link for the image. It should be set on the image that is the Largest Contentful Paint (LCP) element, typically the hero image above the fold. Only one image per page should have priority; setting it on multiple images causes them to compete for preload bandwidth and can delay the actual LCP image .

// Hero image: LCP candidate, above the fold
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority
  sizes="100vw"
/>

Every other image on the page should remain lazy-loaded (the default). The browser loads them only when they approach the viewport, which reduces initial page weight and speeds up the LCP image’s download.


Complete Example Session

// ============================================
// PART 1: LOCAL IMAGE WITH STATIC IMPORT
// ============================================
import Image from 'next/image'
import ProfileImage from './profile.png'

export default function Profile() {
  return (
    <Image
      src={ProfileImage}
      alt="Profile picture"
      placeholder="blur"
    />
  )
}

// width, height, and blurDataURL are inferred.
// No manual dimensions required.
// ============================================
// PART 2: LOCAL IMAGE WITH PATH REFERENCE
// ============================================
<Image
  src="/profile.png"
  alt="Profile picture"
  width={500}
  height={500}
/>

// Path references require explicit dimensions.
// The file must be under public/.
// ============================================
// PART 3: REMOTE IMAGE
// ============================================
<Image
  src="https://s3.amazonaws.com/my-bucket/profile.png"
  alt="Remote profile"
  width={500}
  height={500}
/>

// Remote images require dimensions manually.
// The hostname must be allow-listed.
// ============================================
// PART 4: REMOTE PATTERN CONFIGURATION
// ============================================
// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

export default config
// ============================================
// PART 5: FILL MODE
// ============================================
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    sizes="100vw"
    style={{ objectFit: 'cover' }}
  />
</div>

// The parent must have position: relative.
// fill makes the image expand to the parent.
// ============================================
// PART 6: RESPONSIVE SIZES
// ============================================
<Image
  src="/card.jpg"
  alt="Card"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

// Mobile: 100vw
// Tablet: 50vw
// Desktop: 33vw
// ============================================
// PART 7: PRIORITY FOR LCP
// ============================================
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority
  sizes="100vw"
/>

// Only one image per page should have priority.
// It preloads and disables lazy loading.
// ============================================
// PART 8: TUNING DEVICE SIZES
// ============================================
const config: NextConfig = {
  images: {
    deviceSizes: [640, 750, 1080, 1920],
    imageSizes: [16, 32, 64, 128],
    formats: ['image/avif', 'image/webp'],
  },
}

// Fewer variants = fewer transformations.
// Match the design's actual breakpoints.
// ============================================
// PART 9: THE MISSING sizes PROBLEM
// ============================================
// Wrong: browser assumes 100vw
<Image src="/grid.jpg" alt="Grid item" fill />

// Correct: tells the browser the actual width
<Image
  src="/grid.jpg"
  alt="Grid item"
  fill
  sizes="(max-width: 768px) 50vw, 25vw"
/>
// ============================================
// PART 10: AVOIDING OVER-OPTIMIZATION
// ============================================
// Small icons and logos do not benefit from next/image.
// Serve them as plain <img> or with unoptimized.

<Image src="/logo.svg" alt="Logo" width={32} height={32} unoptimized />

// Static assets that are already optimized
// should not consume transformation quota.

The ten parts covered static imports, path references, remote images, remote patterns, fill mode, responsive sizes, priority, device size tuning, the missing sizes problem, and when to skip optimization.


Quick Reference

Core props

PropRequiredPurpose
srcYesImage path, URL, or static import
altYesAccessibility text
width / heightUnless fillIntrinsic dimensions
fillAlternative to dimensionsExpand to parent size
sizesFor responsiveViewport-width hint for srcset
priorityFor LCPPreload, disable lazy loading
placeholderOptionalblur or empty
blurDataURLWith placeholder="blur" for remoteLow-quality preview
qualityOptional1–100 (default 75; v16 requires allow-list)
unoptimizedOptionalBypass optimization

Local vs remote

SourceDimensionsBlur dataConfig
Static importAutomaticAutomaticNone
public/ pathManualManualNone
Remote URLManualManualremotePatterns

next.config.ts image settings

SettingPurpose
remotePatternsAllow-list remote hostnames
deviceSizesWidths for srcset (viewport-based)
imageSizesWidths for fixed-size images
formatsPreferred output formats
minimumCacheTTLCache duration (v16 default: 14400s)
qualitiesAllow-listed quality values (v16)

Responsive sizing examples

Layoutsizes value
Full-width hero100vw
Card grid (3 columns)(max-width: 768px) 100vw, 33vw
Sidebar thumbnail(max-width: 1024px) 100vw, 200px
Avatar48px

Best Practices

✅ Do This:

// Always provide sizes for responsive images
<Image fill sizes="(max-width: 768px) 100vw, 50vw" />    # ✅
// Use static imports for local images
import Hero from './hero.jpg'
<Image src={Hero} alt="Hero" placeholder="blur" />        # ✅
// Set priority on exactly one LCP image
<Image src="/hero.jpg" priority sizes="100vw" />          # ✅
// Tighten deviceSizes to actual breakpoints
deviceSizes: [640, 1080, 1920]                            # ✅

❌ Don’t Do This:

// Use fill without sizes
<Image src="/grid.jpg" fill />                            # ❌ (downloads largest)
// Set priority on every image
<Image priority /> <Image priority />                     # ❌ (competes for bandwidth)
// Allow any remote hostname
remotePatterns: [{ hostname: '**' }]                      # ❌ (security risk)
// Use next/image for tiny icons
<Image src="/icon.svg" width={16} height={16} />          # ❌ (no benefit)

Common Pitfalls

PitfallWhy It HappensFix
Image downloads oversized variantsizes missing or wrongAdd accurate sizes matching layout
Layout shift still occursNo width/height and no fillProvide dimensions or use fill with positioned parent
Remote image failsHostname not in remotePatternsAdd the pattern to next.config.ts
Format negotiation does not workProxy/CDN strips Accept headerForward Accept header through proxy
LCP image loads slowlypriority missing or set on wrong imageSet priority on the above-the-fold LCP image only
Transform quota exhaustedToo many deviceSizes or missing sizesTighten deviceSizes; add accurate sizes
quality prop ignored in v16Value not in images.qualitiesAdd the quality value to the allow-list

Real-World Examples

1. Static import with blur

import Profile from './profile.png'
<Image src={Profile} alt="Profile" placeholder="blur" />

2. Hero with priority

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

3. Card in a grid

<Image src="/card.jpg" alt="Card" fill sizes="(max-width: 768px) 100vw, 33vw" />

4. Remote image with pattern

remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com', pathname: '/img/**' }]

5. Avatar with fixed size

<Image src="/avatar.jpg" alt="Avatar" width={48} height={48} sizes="48px" />

6. Fill with object-fit

<div style={{ position: 'relative', height: 400 }}>
  <Image src="/bg.jpg" alt="" fill style={{ objectFit: 'cover' }} sizes="100vw" />
</div>

7. Custom loader for CDN

loader: 'custom', loaderFile: './lib/image-loader.ts'

8. Unoptimized for SVG

<Image src="/logo.svg" alt="Logo" width={32} height={32} unoptimized />

9. Dynamic import in Server Component

const { default: image } = await import(`./images/${filename}`)
<Image src={image} alt="Dynamic" />

10. Format configuration

formats: ['image/avif', 'image/webp']

Visual

The optimization pipeline

┌─────────────────────────────────────────────────────────────┐
│  NEXT/IMAGE PIPELINE                                         │
│                                                              │
│  Source image (JPEG/PNG)                                     │
│       │                                                      │
│       ▼                                                      │
│  Build/request time:                                         │
│    ├── Resize to deviceSizes + imageSizes variants           │
│    ├── Convert to WebP/AVIF (based on Accept header)         │
│    └── Generate srcset with width descriptors                │
│       │                                                      │
│       ▼                                                      │
│  Browser:                                                    │
│    ├── Reads sizes attribute                                 │
│    ├── Calculates required width                             │
│    ├── Selects smallest satisfying variant from srcset       │
│    └── Downloads that variant                                │
└─────────────────────────────────────────────────────────────┘

sizes drives selection

┌─────────────────────────────────────────────────────────────┐
│  SIZES HINT                                                  │
│                                                              │
│  sizes="(max-width: 768px) 100vw, 33vw"                      │
│                                                              │
│  Viewport 375px:                                             │
│    100vw = 375px → browser picks 375px variant              │
│                                                              │
│  Viewport 1200px:                                            │
│    33vw = 396px → browser picks 396px variant               │
│                                                              │
│  Without sizes:                                              │
│    Browser assumes 100vw = 1200px → downloads 1200px        │
│    (3x larger than needed)                                   │
└─────────────────────────────────────────────────────────────┘

Priority vs lazy loading

┌─────────────────────────────────────────────────────────────┐
│  LOADING BEHAVIOR                                            │
│                                                              │
│  priority={true}:                                            │
│    ├── Disables lazy loading                                 │
│    ├── Adds <link rel="preload">                             │
│    └── Loads immediately                                     │
│                                                              │
│  priority={false} (default):                                 │
│    ├── loading="lazy"                                        │
│    ├── Loads when near viewport                              │
│    └── Saves initial bandwidth                               │
│                                                              │
│  Only ONE image per page should have priority.               │
└─────────────────────────────────────────────────────────────┘

Local vs remote setup

┌─────────────────────────────────────────────────────────────┐
│  LOCAL                          REMOTE                       │
│                                                              │
│  public/ or static import       URL string                   │
│  Dimensions: auto (import)      Dimensions: manual           │
│  Blur: auto (import)            Blur: manual blurDataURL    │
│  Config: none                   Config: remotePatterns       │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Component importimport Image from 'next/image'
Local static importAuto width, height, blurDataURL
Remote imageManual width/height + remotePatterns
Fill modeParent position: relative, use sizes
Responsive sizingsizes prop drives srcset selection
LCP optimizationpriority on one above-the-fold image
Format negotiationAccept header; proxy must forward it
Device size tuningdeviceSizes + imageSizes in config
v16 changeimages.domains removed; qualities allow-list

Key takeaways:

  • next/image solves three problems: layout shift, oversized downloads, and format selection. The width/height or fill props reserve space. The sizes prop drives responsive selection. The Accept header drives format negotiation .
  • sizes is the most commonly missed prop. Without it, the browser assumes 100vw and downloads the largest variant. A card image in a three-column grid needs sizes="(max-width: 768px) 100vw, 33vw", not the default .
  • priority is for the LCP image only. Setting it on multiple images causes them to compete for preload bandwidth. The hero image above the fold gets priority; everything else stays lazy .
  • Remote images require remotePatterns. The images.domains option was removed in Next.js 16. The pattern should be specific to the actual hostnames and paths the application uses .
  • The transform quota is a budget concern. Every unique source, width, and format combination counts. Tightening deviceSizes and providing accurate sizes reduces the number of transformations .
  • Format negotiation depends on the Accept header. If a proxy or CDN sits in front of Next.js and strips the header, the image is served in its original format and the optimization benefit is lost .

Remember: next/image is an optimization pipeline, not just a tag. The pipeline has three levers: the dimensions (for layout stability), the sizes hint (for responsive selection), and the config (for allowed sources and generated variants). Get the sizes wrong and the browser downloads the wrong image. Set priority on the wrong image and the real LCP image loads late. Forget the remotePatterns entry and the remote image fails outright. Use static imports for local images, provide accurate sizes for responsive layouts, set priority on exactly one image, and keep the deviceSizes array aligned with the design’s actual breakpoints.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!