Image optimization (next/image) in Next.js
next/image is the Next.js Image component that automatically converts images to WebP/AVIF, generates device-specific size variants via srcset, and lazy-loads them by default.
Theory
TL;DR
- Regular
<img>ships the full original file to every device;next/imagegenerates exact-fit variants and delivers only what fits the screen - Main gain: 35-50% smaller files on mobile, because the browser downloads a 400px WebP instead of a 2000px JPEG
- Default behavior: lazy loading, format conversion, CLS prevention via reserved dimensions
priority={true}only for LCP images (hero banners, above-the-fold content); use it on 1-2 images per page at most- External images require
remotePatternsinnext.config.jsor you get a runtime error
Quick example
import Image from 'next/image'
// Regular img: downloads the full 2000x1200px original every time (~2MB)
<img src="/hero.jpg" alt="Hero" width={2000} height={1200} />
// next/image: serves ~300KB WebP at actual device size, lazy by default
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={2000}
height={1200}
priority // above the fold, so preload it
/>
)
}
// Mobile gets a 400w WebP; desktop gets 1200w; browser picks via srcsetOne thing worth noting from production: skipping the sizes prop on a fill image means the browser defaults to 100vw at every breakpoint, so a sidebar thumbnail ends up downloading a full-width variant. Small oversight, real bandwidth cost.
Key difference
The browser has no idea how big your <img> will actually render until it downloads the full CSS and calculates layout. So it plays safe and fetches the original. next/image generates 8+ sized variants at build time and outputs a <picture> element with srcset descriptors. The browser picks the right variant before the download starts, using w descriptors and your sizes hint. No guessing, no waste.
When to use
- Hero and above-the-fold images: add
priorityto skip lazy loading and inject a<link rel="preload">in the<head> - Gallery and card images: default lazy loading with
sizesset to the actual rendered width (e.g."(max-width: 768px) 100vw, 33vw") - Images with dimensions unknown at build time:
fillprop inside a positioned parent container - External URLs from Cloudinary, Unsplash, GitHub avatars: configure
remotePatternsinnext.config.js - Dynamic images with a loading state:
placeholder="blur"withblurDataURLfor a smooth transition
How it works internally
At build time Next.js uses Sharp to generate WebP, AVIF, and JPEG variants at multiple sizes (roughly 25%, 50%, 75%, and 100% of the original, plus device-specific breakpoints). These get cached in .next/cache/images. For placeholder="blur", it encodes a tiny base64 blurhash. At runtime the component outputs a <picture> element with MIME-typed srcset entries: image/avif first, then image/webp, then the original format. The browser picks via the Accept header.
AVIF gives about 75% smaller files than JPEG. WebP gives around 30-50%. Next.js tries AVIF first through content negotiation, falls back to WebP, then JPEG. ISR and SSG both use the same cache, so repeat requests skip Sharp entirely.
Common mistakes
Missing width and height
// Wrong: no optimization, layout shift, console error
<Image src="/me.jpg" alt="Me" />
// Fix: always provide dimensions
<Image src="/me.jpg" alt="Me" width={800} height={600} />fill without a positioned parent
// Wrong: image disappears or overflows
<div>
<Image src="/photo.jpg" fill alt="Photo" />
</div>
// Fix: parent needs position: relative and explicit dimensions
<div className="relative w-full h-64">
<Image src="/photo.jpg" fill alt="Photo" className="object-cover" />
</div>External URL without remotePatterns
// Throws at runtime: "hostname is not configured under images"
// Fix in next.config.js:
module.exports = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' }
]
}
}priority on every image. That loads everything upfront, defeats lazy loading, and slows initial page load. priority belongs only on the LCP element. On most pages that is one image.
sizes="100vw" on grid items. A 3-column card grid renders each card at roughly 33vw on desktop. sizes="100vw" makes the browser download the 1200px variant when it only needs 400px. Set sizes to match the actual rendered width.
Real-world usage
- Vercel homepage: hero banners with
priorityand breakpoint-specificsizes - Stripe docs: code screenshot galleries using
filland blur placeholders - TailwindUI: product cards pulling images from Cloudinary via
remotePatterns - GitHub: user avatars served from
avatars.githubusercontent.comconfigured inremotePatterns - Regular
<img>still makes sense for SVGs and base64 icons under 10KB, or when passingunoptimized={true}to skip the pipeline entirely
Follow-up questions
Q: How does next/image choose between AVIF and WebP?
A: It checks the browser's Accept header. If image/avif is listed, it serves AVIF (about 75% smaller than JPEG). If not, it tries WebP. Falls back to the original format last.
Q: What does the sizes prop actually do?
A: It tells the browser how wide the image will render at each breakpoint, before the CSS loads. Without it, the browser assumes 100vw and downloads a larger variant than needed. For a 300px sidebar image, sizes="300px" cuts the download noticeably.
Q: Difference between placeholder="blur" and placeholder="empty"?
A: blur shows a low-res base64 preview while the full image loads. empty shows nothing. Use blur for large hero images where a visible loading state improves perceived speed. Use empty for small thumbnails where the blurred preview would look worse than just waiting.
Q: Why does next/image sometimes increase build time significantly?
A: Sharp processes every unique image at every size variant. A gallery with 200 images and 8 size breakpoints means 1600 Sharp operations. Setting images.minimumCacheTTL keeps cached variants alive across deploys, and using priority selectively limits what gets pre-processed.
Q: (Senior) You have ISR pages with dynamic product images in S3. Optimization runs at request time but you need responses under 100ms. How do you handle this?
A: Two options. First, configure a custom loader pointing to CloudFront with Lambda@Edge for on-the-fly resizing; Next.js skips its own Sharp pipeline and lets the CDN handle it. Second, limit generated variants with deviceSizes and imageSizes in next.config.js, then warm the cache with a post-deploy script hitting each ISR path. The .next/cache/images directory persists across deploys on Vercel by default.
Examples
Basic: Profile card with a local image
import Image from 'next/image'
export default function ProfileCard({ user }) {
return (
<div className="flex items-center gap-4">
<Image
src="/avatars/default.png"
alt={user.name}
width={64}
height={64}
className="rounded-full"
/>
<p>{user.name}</p>
</div>
)
}
// Next.js serves a 64x64 WebP variant; the original stays on disk untouchedwidth and height match the rendered size, so no oversized download happens. No priority because this card is never above the fold.
Intermediate: Blog post card with responsive fill
import Image from 'next/image'
export default function BlogCard({ post }) {
return (
<div className="relative w-full h-64">
<Image
src={post.coverImage}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover rounded-lg"
placeholder="blur"
blurDataURL={post.blurDataURL}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50" />
</div>
)
}
// Mobile: downloads ~390w WebP
// Tablet: ~640w WebP
// Desktop 3-column grid: ~400w WebPThe sizes prop is doing the real work here. Without it, every device downloads the widest variant. With it, mobile gets a file sized for mobile.
Advanced: External image with a custom loader for Unsplash
// next.config.js
module.exports = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' }
]
}
}import Image from 'next/image'
// Custom loader passes Unsplash's own resize API params
function unsplashLoader({ src, width, quality }) {
return `${src}?w=${width}&q=${quality || 75}&auto=format`
}
export default function UnsplashPhoto({ id, alt }) {
return (
<Image
loader={unsplashLoader}
src={`https://images.unsplash.com/photo-${id}`}
alt={alt}
width={1200}
height={800}
quality={75}
/>
)
}
// Output URL: https://images.unsplash.com/photo-123?w=1200&q=75&auto=format
// Note: custom loader bypasses Next.js Sharp; Unsplash CDN handles resizingThe loader prop replaces Next.js's default URL builder. Useful when the image source has its own resize API (Cloudinary, Imgix, Unsplash). The remotePatterns config is still required even with a custom loader.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.