Introduction
The next/image component is Next.js's built-in solution for one of the web's most persistent performance problems: unoptimized images. It replaces the standard HTML <img> tag with an intelligent component that automatically serves images in modern formats (WebP, AVIF), resizes them for each device, lazy-loads below-the-fold images, and prevents layout shifts — all without manual configuration.
Before next/image, optimizing images required setting up an image CDN, manually writing srcset attributes, implementing lazy loading with Intersection Observer, and generating multiple image sizes at build time. Now, a single component declaration handles all of this automatically, resulting in measurable improvements to Core Web Vitals and user experience.
This guide covers everything from basic usage to advanced patterns, including custom loaders, blur placeholders, and integration with third-party image services.
Understanding next/image: Core Concepts
Basic Usage
The simplest usage requires src, alt, and either width/height or fill:
import Image from "next/image";
// Fixed dimensions
<Image
src="/profile.jpg"
alt="Profile photo"
width={200}
height={200}
/>
// Fill parent container
<div className="relative w-full h-64">
<Image
src="/banner.jpg"
alt="Banner"
fill
className="object-cover"
/>
</div>What Happens Under the Hood
When you render <Image src="/photo.jpg" width={800} height={600} />, Next.js:
- Generates a
<picture>element with multiple<source>entries for AVIF and WebP formats - Creates
srcsetwith multiple widths (640, 750, 828, 1080, 1200, 1920) - Adds
sizes="100vw"as a default (you should override this) - Sets
loading="lazy"by default (images load as they approach the viewport) - Adds
decoding="async"for non-blocking decode - Reserves space with explicit
widthandheightto prevent layout shift - Creates an optimization endpoint (
/_next/image?url=...&w=...&q=...) that serves optimized images
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | Required | Image source URL or static import |
alt | string | Required | Alternative text for accessibility |
width | number | — | Intrinsic width in pixels |
height | number | — | Intrinsic height in pixels |
fill | boolean | false | Fill parent container (needs position: relative) |
sizes | string | "100vw" | Responsive sizes for srcset |
quality | number | 75 | Output quality (1-100) |
priority | boolean | false | Disable lazy loading, add fetchpriority="high" |
placeholder | string | "empty" | "empty" or "blur" |
blurDataURL | string | — | Base64 blur placeholder (auto-generated for static imports) |
loading | string | "lazy" | "lazy" or "eager" |
loader | function | default | Custom image loader function |
Architecture and Design Patterns
Pattern 1: Responsive Image with Proper Sizes
The most impactful optimization you can make is setting the sizes attribute correctly:
// Blog article image — takes full width on mobile, max 720px on desktop
<Image
src={post.coverImage}
alt={post.title}
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, 720px"
/>
// Product card in a 4-column grid
<Image
src={product.image}
alt={product.name}
width={400}
height={400}
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
// Hero image that spans full viewport
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
sizes="100vw"
priority
/>Without sizes, the browser defaults to 100vw and may download a 1920px image for a 400px card.
Pattern 2: Blur Placeholder for Progressive Loading
Static imports automatically generate blur data:
import heroImage from "@/public/hero.jpg";
<Image
src={heroImage}
alt="Hero"
placeholder="blur"
priority
/>For external URLs, generate the blur data server-side:
import { getPlaiceholder } from "plaiceholder";
export async function BlurredImage({ src, alt }: { src: string; alt: string }) {
const buffer = await fetch(src).then(r => r.arrayBuffer());
const { base64 } = await getPlaiceholder(Buffer.from(buffer));
return (
<Image
src={src}
alt={alt}
fill
placeholder="blur"
blurDataURL={base64}
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}Pattern 3: Image Gallery with Zoom
"use client";
import Image from "next/image";
import { useState } from "react";
export function ImageGallery({ images }: { images: string[] }) {
const [active, setActive] = useState(0);
const [zoomed, setZoomed] = useState(false);
return (
<div className="space-y-4">
<div
className={`relative cursor-zoom-in ${zoomed ? "cursor-zoom-out" : ""}`}
onClick={() => setZoomed(!zoomed)}
>
<div className={`relative ${zoomed ? "h-[80vh]" : "aspect-square"} overflow-hidden rounded-lg`}>
<Image
src={images[active]}
alt={`Image ${active + 1}`}
fill
sizes="(max-width: 768px) 100vw, 800px"
className={`object-contain transition-transform duration-300 ${
zoomed ? "scale-150" : "scale-100"
}`}
priority={active === 0}
/>
</div>
</div>
<div className="flex gap-2">
{images.map((src, i) => (
<button
key={i}
onClick={() => { setActive(i); setZoomed(false); }}
className={`relative w-16 h-16 rounded border-2 overflow-hidden ${
i === active ? "border-blue-500" : "border-gray-200"
}`}
>
<Image src={src} alt={`Thumbnail ${i + 1}`} fill sizes="64px" className="object-cover" />
</button>
))}
</div>
</div>
);
}Step-by-Step Implementation
Step 1: Configure Remote Image Domains
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "cdn.your-app.com",
pathname: "/uploads/**",
},
],
// Enable modern formats
formats: ["image/avif", "image/webp"],
// Device breakpoints for srcset
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
// Icon/thumbnail breakpoints
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
export default nextConfig;Step 2: Create a Reusable Image Component
// components/ui/OptimizedImage.tsx
import Image, { type ImageProps } from "next/image";
interface OptimizedImageProps extends Omit<ImageProps, "sizes"> {
variant?: "hero" | "card" | "thumbnail" | "full";
}
const sizeMap = {
hero: "100vw",
card: "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw",
thumbnail: "(max-width: 640px) 50vw, 150px",
full: "(max-width: 768px) 100vw, 720px",
};
export function OptimizedImage({ variant = "full", ...props }: OptimizedImageProps) {
return (
<Image
{...props}
sizes={sizeMap[variant]}
quality={variant === "hero" ? 85 : 75}
/>
);
}Step 3: Implement Background Images
export function BackgroundImage({ src, alt, children }: {
src: string;
alt: string;
children: React.ReactNode;
}) {
return (
<div className="relative min-h-screen">
<Image
src={src}
alt={alt}
fill
sizes="100vw"
quality={80}
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-black/50" />
<div className="relative z-10">{children}</div>
</div>
);
}Step 4: Handle Errors Gracefully
"use client";
import Image from "next/image";
import { useState } from "react";
export function SafeImage({ src, alt, fallback = "/placeholder.jpg", ...props }: {
src: string;
alt: string;
fallback?: string;
} & Omit<React.ComponentProps<typeof Image>, "src" | "alt">) {
const [imgSrc, setImgSrc] = useState(src);
return (
<Image
{...props}
src={imgSrc}
alt={alt}
onError={() => setImgSrc(fallback)}
/>
);
}Real-World Use Cases
Use Case 1: Social Media Feed
A social media feed displays user-uploaded images of varying dimensions. Using fill with object-cover ensures all images display at consistent sizes without distortion. Lazy loading prevents the browser from downloading all images at once — only images within 250px of the viewport are loaded. The result: 85% reduction in initial page weight for feeds with 50+ images.
Use Case 2: Real Estate Listings
A real estate site uses hero images with priority for the first listing, blur placeholders for all listing photos, and responsive sizes that serve 400px-wide images on mobile (card view) and 800px images on desktop (detail view). Combined with AVIF format, the site loads 3x faster than the previous implementation using raw <img> tags.
Use Case 3: Documentation Site
A technical documentation site uses next/image for diagrams and screenshots. Each image has a descriptive alt text, appropriate sizes for the content column width, and a shimmer placeholder that matches the site's design system. The site achieves a perfect 100 Lighthouse accessibility score while maintaining excellent performance.
Best Practices for Production
-
Always set
sizes: The default100vwcauses the browser to download images larger than needed. Matchsizesto your CSS layout. -
Use
priorityfor LCP images: The first visible image (hero, product photo, article thumbnail) should havepriorityto optimize LCP. -
Set
widthandheight: These prevent layout shifts by reserving space before the image loads. If dimensions are unknown, usefillwith a sized parent. -
Enable AVIF: Add
formats: ["image/avif", "image/webp"]tonext.config.ts. AVIF is 30% smaller than WebP. -
Use blur placeholders for hero images:
placeholder="blur"withblurDataURLgives users a preview while the full image loads. -
Don't use
priorityon every image: Only above-the-fold images should havepriority. Using it on all images defeats lazy loading. -
Use static imports when possible:
import img from "@/public/photo.jpg"auto-generates blur data and enables build-time optimization. -
Set quality appropriately: Use 85-90 for hero/detail images, 60-75 for thumbnails and cards.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
Missing sizes | Oversized images downloaded on mobile | Set sizes matching your CSS layout |
No width/height on non-fill images | Layout shift (CLS) | Always set dimensions or use fill |
| External domain not configured | 403 error | Add to remotePatterns in next.config.ts |
Using priority on all images | No lazy loading, slow initial load | Only use on above-the-fold images |
Forgetting object-cover with fill | Distorted images | Always add className="object-cover" or object-contain |
| Large source images | Slow optimization | Pre-compress to reasonable sizes (max 2-3MB) |
Performance Optimization
// Complete optimized image setup
import Image from "next/image";
import heroImg from "@/public/hero.jpg";
// Hero: priority, blur, high quality
<Image
src={heroImg}
alt="Hero banner"
priority
placeholder="blur"
quality={90}
sizes="100vw"
className="object-cover"
/>
// Product card: lazy, standard quality, responsive
<Image
src={product.image}
alt={product.name}
width={400}
height={400}
quality={75}
sizes="(max-width: 640px) 50vw, 25vw"
className="object-cover rounded-lg"
/>
// Thumbnail: lazy, lower quality, small size
<Image
src={thumbnail}
alt="Thumbnail"
width={80}
height={80}
quality={60}
sizes="80px"
className="rounded-full"
/>Comparison with Alternatives
| Feature | next/image | Cloudinary | Imgix | Manual <img> |
|---|---|---|---|---|
| Format negotiation | Automatic | Via URL params | Via URL params | Manual |
| Responsive srcset | Automatic | Manual | Manual | Manual |
| Lazy loading | Built-in | None | None | Intersection Observer |
| Blur placeholder | Built-in | Via URL params | Via URL params | Manual |
| Layout shift prevention | Built-in | None | None | Manual |
| Setup effort | Zero | Medium | Medium | High |
Image Preloading and Resource Hints
Beyond priority, you can preload critical images using <link rel="preload"> in your layout for images that next/image can't detect (like CSS background images or images loaded via JavaScript):
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<link
rel="preload"
as="image"
href="/hero.jpg"
imagesrcset="/hero-640.avif 640w, /hero-1080.avif 1080w, /hero-1920.avif 1920w"
imagesizes="100vw"
type="image/avif"
/>
</head>
<body>{children}</body>
</html>
);
}This ensures the browser starts downloading the hero image before parsing JavaScript, giving you the fastest possible LCP. Combine with fetchpriority="high" on the image element for maximum effect.
For font files associated with image-heavy pages (like design portfolios), preload them too to avoid the flash of unstyled text while images load.
Advanced Patterns
Custom Loader for AWS S3 + CloudFront
// lib/s3-loader.ts
export function s3Loader({ src, width, quality }: {
src: string;
width: number;
quality?: number;
}) {
return `https://cdn.example.com/cdn-cgi/image/width=${width},quality=${quality || 75}${src}`;
}
// next.config.ts
const nextConfig = {
images: {
loader: "custom",
loaderFile: "./lib/s3-loader.ts",
},
};SVG Component Integration
// For SVGs, use regular <img> or import as React components
import Logo from "@/public/logo.svg";
export function SiteLogo() {
return (
<Image
src={Logo}
alt="Company Logo"
width={120}
height={40}
priority
/>
);
}Image CDN Integration
For applications serving millions of images, offloading optimization to a dedicated CDN provides better global performance and reduces load on your Next.js server. Cloudinary, Imgix, and Bunny.net are popular choices that integrate with next/image via custom loaders.
Cloudinary Integration
// lib/cloudinary-loader.ts
export function cloudinaryLoader({
src,
width,
quality,
}: {
src: string;
width: number;
quality?: number;
}) {
const params = [
`f_auto`,
`c_limit`,
`w_${width}`,
`q_${quality || "auto"}`,
];
return `https://res.cloudinary.com/your-cloud/image/upload/${params.join(",")}${src}`;
}
// Usage in next.config.ts
const nextConfig = {
images: {
loader: "custom",
loaderFile: "./lib/cloudinary-loader.ts",
remotePatterns: [
{
protocol: "https",
hostname: "res.cloudinary.com",
},
],
},
};Cloudinary's f_auto parameter automatically serves AVIF to supported browsers, WebP to Chrome/Firefox, and JPEG as fallback — matching what next/image does natively but with global edge caching.
Bunny.net CDN with Next.js
// lib/bunny-loader.ts
export function bunnyLoader({ src, width, quality }: {
src: string;
width: number;
quality?: number;
}) {
return `https://your-zone.b-cdn.net${src}?width=${width}&quality=${quality || 75}&format=auto`;
}Bunny.net offers per-pixel pricing that's significantly cheaper than Cloudinary for high-traffic sites. Their edge storage puts images within 30ms of 95% of global users.
Dynamic OG Image Generation
For social sharing, generate Open Graph images dynamically using @vercel/og:
// app/api/og/route.tsx
import { ImageResponse } from "next/og";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") || "Default Title";
return new ImageResponse(
(
<div
style={{
display: "flex",
height: "100%",
width: "100%",
alignItems: "center",
justifyContent: "center",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
}}
>
<h1 style={{ fontSize: 64, color: "white", textAlign: "center" }}>
{title}
</h1>
</div>
),
{ width: 1200, height: 630 }
);
}Then reference in your layout: <meta property="og:image" content="https://yourdomain.com/api/og?title=My+Post" />. This eliminates the need to pre-generate and store OG images for every page.
Measuring Image Performance
Core Web Vitals Impact
Images directly affect all three Core Web Vitals. The largest contentful paint (LCP) element is an image on over 70% of web pages. Cumulative layout shift (CLS) is often caused by images without explicit dimensions. Even interaction to next paint (INP) can be affected by heavy image decoding that blocks the main thread.
Use Lighthouse CI in your build pipeline to catch image regressions before they reach production:
// lighthouse-ci.config.js
module.exports = {
ci: {
assert: {
assertions: {
"largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
"cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }],
"uses-responsive-images": ["warn"],
"uses-optimized-images": ["warn"],
"modern-image-formats": ["warn"],
},
},
},
};Debugging Image Loading Issues
When images aren't performing as expected, the browser DevTools Network tab reveals the full picture. Filter by "Img" to see all image requests, their sizes, formats, and loading waterfall. Look for these common issues:
- Missing
sizesattribute: The browser downloads a larger image than displayed. Check the "Initiator" column — if it showssizes="(max-width: ...)..."is missing, the browser defaults to 100vw. - Wrong
priorityusage: LCP images without priority load late. Compare the image's start time against DOMContentLoaded. - Format fallback: If you see JPEG instead of AVIF/WebP, check your
next.config.tsformat configuration and verify the image optimizer is running (not disabled viaunoptimized: true).
The next/image component logs warnings in development mode for common mistakes like missing alt text, incorrect width/height on static imports, and oversized external images. These warnings are suppressed in production, so always check your development console.
Image CDN Analytics
If you use an external image CDN (Cloudinary, Imgix, Bunny.net), their analytics dashboards provide insights that Next.js alone cannot:
- Format distribution: What percentage of requests serve AVIF vs WebP vs JPEG? Aim for 80%+ modern formats.
- Cache hit rate: Should be above 95% for production traffic. Low rates indicate cache configuration issues.
- Bandwidth savings: Compare original image sizes vs delivered sizes. The delta is your optimization savings.
- Error rates: 404s on image requests often indicate broken image references or expired signed URLs.
// Example: Track image performance in your app
function trackImageMetrics() {
if (typeof window === "undefined") return;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.element?.tagName === "IMG") {
const img = entry.element as HTMLImageElement;
console.log({
src: img.src,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
displayWidth: img.clientWidth,
displayHeight: img.clientHeight,
loadTime: entry.startTime + entry.duration,
format: img.currentSrc.match(/\.(\w+)$/)?.[1] || "unknown",
});
}
}
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
}Testing Strategies
import { render, screen } from "@testing-library/react";
import Image from "next/image";
describe("next/image Component", () => {
it("renders with correct attributes", () => {
render(<Image src="/test.jpg" alt="Test" width={800} height={600} />);
const img = screen.getByAltText("Test");
expect(img).toHaveAttribute("width", "800");
expect(img).toHaveAttribute("height", "600");
});
it("applies lazy loading by default", () => {
render(<Image src="/test.jpg" alt="Test" width={800} height={600} />);
const img = screen.getByAltText("Test");
expect(img).toHaveAttribute("loading", "lazy");
});
it("applies eager loading with priority", () => {
render(<Image src="/hero.jpg" alt="Hero" width={1920} height={1080} priority />);
const img = screen.getByAltText("Hero");
expect(img).toHaveAttribute("loading", "eager");
});
it("generates srcset with multiple widths", () => {
render(<Image src="/photo.jpg" alt="Photo" width={1200} height={600} />);
const img = screen.getByAltText("Photo");
const srcset = img.getAttribute("srcset");
expect(srcset).toBeTruthy();
expect(srcset).toContain("640w");
expect(srcset).toContain("1200w");
});
});Future Outlook
The next/image component continues to evolve with each Next.js release. Recent improvements include better support for SVG images, improved caching strategies, and integration with React Suspense for streaming image loading. The React team's exploration of <Image> as a first-class React primitive may eventually provide native browser-level image optimization that builds on the patterns Next.js pioneered.
As AVIF adoption grows and new formats emerge, Next.js's automatic format negotiation ensures your images will always be served in the most efficient format available — without any code changes.
Conclusion
The next/image component is the single most impactful performance optimization available in Next.js. It handles format negotiation, responsive sizing, lazy loading, and layout shift prevention automatically.
Key takeaways:
- Replace all
<img>withnext/imagefor automatic optimization, responsive sizing, and lazy loading. - Set
sizescorrectly — this is the most common mistake and the easiest performance win. - Use
priorityon LCP images — hero banners, product photos, and article thumbnails above the fold. - Enable AVIF format for 30% smaller images compared to WebP.
- Use blur placeholders for a polished loading experience.
Start by auditing your largest images and applying these optimizations. The performance improvements are immediate and measurable.
For more details, see the Next.js Image documentation.