Next.js 12 ▲ Image Optimization — next/image component & responsive sizing
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
| Prop | Required | Purpose |
|---|---|---|
src | Yes | Image path, URL, or static import |
alt | Yes | Accessibility text |
width / height | Unless fill | Intrinsic dimensions |
fill | Alternative to dimensions | Expand to parent size |
sizes | For responsive | Viewport-width hint for srcset |
priority | For LCP | Preload, disable lazy loading |
placeholder | Optional | blur or empty |
blurDataURL | With placeholder="blur" for remote | Low-quality preview |
quality | Optional | 1–100 (default 75; v16 requires allow-list) |
unoptimized | Optional | Bypass optimization |
Local vs remote
| Source | Dimensions | Blur data | Config |
|---|---|---|---|
| Static import | Automatic | Automatic | None |
public/ path | Manual | Manual | None |
| Remote URL | Manual | Manual | remotePatterns |
next.config.ts image settings
| Setting | Purpose |
|---|---|
remotePatterns | Allow-list remote hostnames |
deviceSizes | Widths for srcset (viewport-based) |
imageSizes | Widths for fixed-size images |
formats | Preferred output formats |
minimumCacheTTL | Cache duration (v16 default: 14400s) |
qualities | Allow-listed quality values (v16) |
Responsive sizing examples
| Layout | sizes value |
|---|---|
| Full-width hero | 100vw |
| Card grid (3 columns) | (max-width: 768px) 100vw, 33vw |
| Sidebar thumbnail | (max-width: 1024px) 100vw, 200px |
| Avatar | 48px |
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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Image downloads oversized variant | sizes missing or wrong | Add accurate sizes matching layout |
| Layout shift still occurs | No width/height and no fill | Provide dimensions or use fill with positioned parent |
| Remote image fails | Hostname not in remotePatterns | Add the pattern to next.config.ts |
| Format negotiation does not work | Proxy/CDN strips Accept header | Forward Accept header through proxy |
| LCP image loads slowly | priority missing or set on wrong image | Set priority on the above-the-fold LCP image only |
| Transform quota exhausted | Too many deviceSizes or missing sizes | Tighten deviceSizes; add accurate sizes |
quality prop ignored in v16 | Value not in images.qualities | Add 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
| Item | Value |
|---|---|
| Component import | import Image from 'next/image' |
| Local static import | Auto width, height, blurDataURL |
| Remote image | Manual width/height + remotePatterns |
| Fill mode | Parent position: relative, use sizes |
| Responsive sizing | sizes prop drives srcset selection |
| LCP optimization | priority on one above-the-fold image |
| Format negotiation | Accept header; proxy must forward it |
| Device size tuning | deviceSizes + imageSizes in config |
| v16 change | images.domains removed; qualities allow-list |
Key takeaways:
next/imagesolves three problems: layout shift, oversized downloads, and format selection. Thewidth/heightorfillprops reserve space. Thesizesprop drives responsive selection. TheAcceptheader drives format negotiation .sizesis the most commonly missed prop. Without it, the browser assumes100vwand downloads the largest variant. A card image in a three-column grid needssizes="(max-width: 768px) 100vw, 33vw", not the default .priorityis for the LCP image only. Setting it on multiple images causes them to compete for preload bandwidth. The hero image above the fold getspriority; everything else stays lazy .- Remote images require
remotePatterns. Theimages.domainsoption 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
deviceSizesand providing accuratesizesreduces the number of transformations . - Format negotiation depends on the
Acceptheader. 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.
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!