Image Optimization with next/image in Next.js 12
TL;DR —
next/imageauto-converts to WebP/AVIF, generates responsive sizes, lazy-loads below the fold, and reserves layout space to prevent CLS. Width + height required (or uselayout="fill"). Domain allow-list for remote images. Single biggest “free” perf win for image-heavy sites.
After middleware, the last Next.js feature post of the month: next/image. It’s the kind of feature that sounds boring until you measure your Lighthouse score before and after. For most marketing/content sites, swapping <img> for <Image> is the single biggest perf win you can make.
This post covers the next/image patterns that matter, the gotchas with remote images, and how to think about the layout and sizes props.
What next/image does for you
When you use <Image> instead of <img>:
- Format conversion: WebP / AVIF served to supporting browsers; original format as fallback. Typically 25–50% smaller files.
- Responsive sizes: multiple resolutions generated, served based on viewport and DPR via
srcset. - Lazy loading: images below the fold load on scroll, not on initial page load.
- Layout preservation: the space is reserved during loading, eliminating Cumulative Layout Shift (CLS).
- Blur placeholder: optional low-res blur while the full image loads.
- CDN caching: optimized variants cached on the platform’s CDN (Vercel) or your own.
All of that for the same JSX call. The trade-off: a slight API constraint (width + height required) and a remote-domain allow-list.
Basic usage — local image
import Image from 'next/image';
import logo from '../public/logo.png';
<Image src={logo} alt="Company logo" />
When importing a local image, Next.js statically analyzes its dimensions. width, height, placeholder="blur" available without specifying.
Basic usage — remote URL
<Image
src="https://cdn.example.com/photo.jpg"
alt="Sunset"
width={1200}
height={800}
/>
For remote sources, you must specify width and height (Next.js can’t statically inspect them). You also need to allow-list the domain in next.config.js:
// next.config.js
module.exports = {
images: {
domains: ['cdn.example.com', 'images.unsplash.com'],
},
};
(Next.js 12.3 introduces remotePatterns for more granular control; in 12.1 we use domains.)
If you try to use a remote image from an undomained host, you get a hard error: “next/image un-configured host” — intentional, prevents accidental SSRF or hot-linking from arbitrary domains.
Layout modes
Five layout values in Next.js 12:
layout="intrinsic" (default): image displays at natural size, scales down on small screens.
layout="fixed": image is exactly width × height, never scales.
layout="responsive": image scales fluidly to fill its parent’s width, maintaining aspect ratio.
layout="fill": image fills the parent container. Requires parent to have position: relative + explicit dimensions.
layout="raw" (experimental): minimal wrapping, more like a vanilla <img> for full control.
For hero images:
<div style={{ position: 'relative', width: '100%', height: '60vh' }}>
<Image
src="/hero.jpg"
alt="Hero"
layout="fill"
objectFit="cover"
priority
/>
</div>
priority opts out of lazy loading — important for above-the-fold images (LCP candidates).
The sizes prop
The sizes attribute tells the browser what size the image will display at across viewports, so it picks the right srcset candidate:
<Image
src="/photo.jpg"
alt="Photo"
layout="responsive"
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
Read: “on mobile (≤768px), this image is 100% of viewport width; on tablet (≤1200px), 50%; otherwise 33%.”
Without sizes, the browser assumes 100vw and downloads the largest variant. With it, the browser picks a smaller variant on smaller layouts. Often a 50% file-size savings on mobile.
Forgetting sizes is the most common reason “I added next/image but didn’t see the win.”
Placeholder strategies
<Image
src={localImport}
alt="..."
placeholder="blur"
/>
For local imports, placeholder="blur" generates a 10×10 base64 blur, shown until the full image loads. Smooth.
For remote images, you’d need to provide blurDataURL yourself — usually generated at build time by your CMS or pipeline.
Skip placeholders for images that load instantly (small thumbnails); they add markup overhead for no benefit.
Priority and lazy loading
By default, images are lazy-loaded — they load when scrolled near the viewport.
For above-the-fold images that affect LCP (Largest Contentful Paint), pass priority:
<Image src="/hero.jpg" alt="..." priority />
priority images:
- Skip lazy loading
- Get a
<link rel="preload">injection - Are prioritized by the browser
Use sparingly — 1 or 2 per page max. Marking everything as priority defeats the purpose.
Self-hosted optimization
If you self-host Next.js (not Vercel), image optimization still works — Next.js runs a small image processing service on the Node server. Issues:
- Requires a Node host (not pure static export)
- Cost: each unique variant pulls 50–200 ms CPU per request, then cached
- Storage: the image cache grows; configure cleanup
For high-traffic self-hosted sites, point loader at an external image CDN (Cloudinary, imgix) that handles transformations:
// next.config.js
module.exports = {
images: {
loader: 'imgix',
path: 'https://example.imgix.net',
},
};
Now Next.js generates URL-based transformations; imgix serves and caches.
Real perf numbers
For our marketing site:
| Metric | <img> |
<Image> |
|---|---|---|
| LCP | 3.2 s | 1.6 s |
| CLS | 0.18 | 0.02 |
| Image bytes (mobile) | 1.8 MB | 420 KB |
| Lighthouse Performance | 64 | 92 |
The biggest win is layout-shift elimination (CLS). The second-biggest is mobile bytes (responsive sizes + WebP).
For a content/marketing site, this is essentially free. The migration is one-line per image (plus the next.config.js allow-list for remote sources).
Common Pitfalls
Forgetting width and height on remote images. Compile error. Always specify.
Using layout="fill" without position: relative on parent. Image overflows or doesn’t render. Wrap properly.
Not setting sizes on responsive images. Browser downloads the largest variant on every viewport. Defeats the win.
Marking every image as priority. All images preload, defeating prioritization. 1–2 per page max.
Hot-linking from a CDN you don’t own. Even allow-listed, you’re hitting someone else’s bandwidth costs. For real production, host images you control.
Optimizing decorative SVGs through next/image. SVGs don’t benefit from WebP/AVIF conversion. Use a regular <img> or inline SVG.
Self-hosted Next.js with no image cache cleanup. The cache grows forever. Configure or use external CDN.
Switching to next/image without measuring. You should see Lighthouse and CLS improve. If you don’t, you’re missing sizes, priority, or have layout issues.
Wrapping Up
next/image is one of those features where the win is large and the work is small. For content-heavy sites it’s the single biggest Lighthouse improvement you can make in an afternoon. Wednesday: Vercel vs self-hosted Next.js deployment — where to actually run all this.