Image Optimization in Next.js
Your Lighthouse score shows LCP at 4.2 seconds. The hero image is a 3.2 MB PNG loaded at full resolution on a 390px mobile screen. The next/image component exists specifically to prevent this: it generates responsive srcsets, serves modern formats, reserves layout space, and lazy-loads below-the-fold content. Misconfiguring it—wrong sizes, missing priority, unlisted remote domains—leaves most of the benefit on the table.
Basic usage
import Image from "next/image";
import hero from "@/public/hero.jpg";
export function Hero() {
return (
<Image
src={hero}
alt="Team collaborating in a bright office"
priority
placeholder="blur"
sizes="100vw"
style={{ width: "100%", height: "auto" }}
/>
);
}
Static imports (import hero from ...) provide automatic width, height, and blurDataURL at build time.
The sizes attribute
sizes tells the browser which srcset entry to download. Getting it wrong wastes bandwidth or serves blurry images.
{/* Image fills full viewport width on mobile, half on desktop */}
<Image
src="/product.jpg"
alt="Product photo"
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, 50vw"
/>
{/* Fixed 200px thumbnail in a grid */}
<Image
src={thumbnail}
alt="Thumbnail"
width={200}
height={200}
sizes="200px"
/>
Rule of thumb: match sizes to your CSS layout, not the source image dimensions.
Remote images
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
pathname: "/images/**",
},
],
},
};
<Image
src="https://cdn.example.com/images/product-42.jpg"
alt="Product 42"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 800px"
/>
Priority and LCP
Any image visible in the initial viewport that is the LCP candidate needs priority:
<Image src={heroImage} alt="Hero" priority sizes="100vw" />
priority adds a <link rel="preload"> and disables lazy loading. Use on one image per page—typically the hero. Adding priority to every image defeats the purpose.
Placeholders
{/* Static import — automatic blur */}
<Image src={photo} alt="..." placeholder="blur" />
{/* Remote image — manual blurDataURL */}
<Image
src="https://cdn.example.com/photo.jpg"
alt="..."
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
Generate blurDataURL at build time with plaiceholder or sharp for remote images.
Fill layout for unknown aspect ratios
<div style={{ position: "relative", width: "100%", aspectRatio: "16/9" }}>
<Image
src={src}
alt="Gallery item"
fill
sizes="(max-width: 768px) 100vw, 33vw"
style={{ objectFit: "cover" }}
/>
</div>
The parent must have position: relative. fill removes the need for explicit width/height on the image.
Custom loader for external CDNs
When Next.js image optimization becomes a bottleneck at scale, delegate to your CDN:
// next.config.js
module.exports = {
images: {
loader: "custom",
loaderFile: "./lib/image-loader.ts",
},
};
// lib/image-loader.ts
export default function cloudinaryLoader({
src, width, quality,
}: { src: string; width: number; quality?: number }) {
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75},f_auto/${src}`;
}
Cloudinary, Imgix, and Cloudflare Images all support URL-parameter transformations.
Performance checklist
- Convert source images to WebP or AVIF before upload; Next.js re-encodes but smaller sources process faster.
- Cap
deviceSizesin config if you never serve above 1920px displays. - Use
quality={75}(default) for photos;quality={90}for graphics with text. - Monitor Image Optimization API usage on Vercel—high-traffic sites hit plan limits.
images: {
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
formats: ["image/avif", "image/webp"],
}
App Router vs Pages Router image handling
App Router (Next.js 13+) changes image optimization defaults:
// app/page.tsx — App Router
import Image from "next/image";
import heroImage from "@/public/hero.jpg"; // static import for blur placeholder
export default function Page() {
return (
<Image
src={heroImage}
alt="Hero"
priority // LCP image — preload, no lazy load
placeholder="blur" // automatic with static import
sizes="100vw"
/>
);
}
Static imports enable automatic blur placeholder generation. priority prop prevents lazy loading on LCP candidates — critical for Core Web Vitals.
Remote image configuration
Secure remote image loading with explicit allowlist:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com", pathname: "/images/**" },
{ protocol: "https", hostname: "*.cloudfront.net" },
],
// Never use domains: [] — deprecated and less secure
},
};
Wildcard hostnames supported in remotePatterns. Never allow hostname: "**" in production — open proxy risk.
LCP optimization checklist
Largest Contentful Paint is usually the hero image:
// LCP optimization pattern
<Image
src="/hero.webp"
alt="Product hero"
width={1920}
height={1080}
priority // 1. disable lazy load
sizes="100vw" // 2. correct sizes attribute
quality={80} // 3. balance quality vs size
placeholder="blur" // 4. blur while loading
blurDataURL={blurDataUrl} // 5. inline blur hash
/>
Preload hint added automatically with priority. Combine with fetchPriority="high" on the Image component for explicit browser hint.
Failure modes
- LCP image lazy loaded — missing
priorityprop; LCP delayed by lazy load - Wrong sizes attribute — browser downloads oversized image; wasted bandwidth
- remotePatterns too permissive — open proxy via image optimization API
- Unoptimized source images — 5MB PNG sources slow optimization pipeline
- quality={100} on all images — unnecessary file size; 75–80 sufficient for photos
Production checklist
- LCP candidate marked with
priorityprop sizesattribute matches actual rendered width- remotePatterns allowlist (no wildcard hostnames)
- Source images in WebP/AVIF before upload
- quality={75} default; quality={90} only for text-heavy graphics
- deviceSizes capped at 1920px unless 4K display support required
Resources
- next/image documentation — props and behavior
- Next.js image configuration — remotePatterns, sizes, formats
- Web.dev LCP guide — Largest Contentful Paint optimization
- Responsive images (MDN) — srcset and sizes fundamentals
- plaiceholder library — generating blur placeholders
Frequently asked questions
Should I use next/image or a regular img tag?
Use next/image for any content image where you control dimensions or aspect ratio. It provides automatic srcset generation, lazy loading, format negotiation (WebP/AVIF), and CLS prevention via reserved space. Use img only for user-uploaded images with unknown dimensions where you cannot set width and height.
How do I optimize images hosted on an external CDN?
Add the domain to images.remotePatterns in next.config.js. Next.js will proxy and transform external images through its optimization API. For high-traffic sites, use a custom loader pointing to your CDN's image transformation service.
Why is my LCP image loading slowly?
LCP images need priority={true} to skip lazy loading and preload early. Set explicit width and height to prevent layout shift. Serve appropriately sized images—an 800px display does not need a 4000px source file.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →