Next.js Image Component Deep Dive: All Props, Performance Impact
A thorough look at every prop in Next.js Image, what each one actually does to your Core Web Vitals, and when to skip the component entirely.
Why the Next.js Image Component Exists
Honestly, most developers drop a plain <img> tag, ship to production, then wonder why their Lighthouse score shows a red LCP. That's the exact problem Next.js Image was built to fix. It's not magic — it's a collection of defaults you'd otherwise have to wire up by hand.
The component wraps your images in an automatic optimization pipeline. It converts to WebP or AVIF depending on browser support, generates multiple sizes for different viewports, and defers loading for off-screen images by default. All without you writing a single line of custom resize logic.
It shipped in Next.js 10, got a significant rewrite in Next.js 13 (the next/image import stayed, but the internals changed), and as of Next.js 15 the layout prop is completely gone. If you're still reading old blog posts that mention layout="responsive", stop. That API doesn't exist anymore.
Required Props: src, alt, width and height
You need four props at minimum: src, alt, width, and height. The width and height aren't just HTML attributes — the optimizer uses them to pre-calculate the image aspect ratio so there's no layout shift while the image loads. That directly affects your CLS score.
The src prop accepts a string path, an imported static image, or a URL from an external domain (with appropriate next.config setup). Static imports are the cleanest option because Next.js reads the file dimensions at build time — you skip the width/height props entirely when you do that.
import heroImage from '@/public/hero.png'
import Image from 'next/image'
// Static import — no width/height needed
export function Hero() {
return (
<Image
src={heroImage}
alt="Empire UI component library hero screenshot"
priority
className="rounded-xl"
/>
)
}
// External URL — width and height required
export function Avatar({ src }: { src: string }) {
return (
<Image
src={src}
alt="User avatar"
width={48}
height={48}
className="rounded-full"
/>
)
}The alt prop is non-optional for accessibility reasons, and it should describe the image content. For decorative images that add zero information, pass an empty string (alt=""). The component won't complain, and screen readers will skip it correctly.
The fill Prop and How to Use It Correctly
When you don't know the intrinsic dimensions ahead of time — think user-uploaded content or a CMS returning arbitrary images — fill is your escape hatch. It replaces the width and height props and makes the image fill its parent container.
The catch: the parent element must have position: relative (or absolute or fixed). Without it, the image renders with 0 height and you'll spend an embarrassing amount of time debugging an invisible image.
export function CoverImage({ src, alt }: { src: string; alt: string }) {
return (
<div className="relative w-full h-64 overflow-hidden rounded-2xl">
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
)
}Notice the sizes prop in that snippet. It's not optional when using fill if you care about performance. Without it, Next.js assumes the image could be 100vw on every screen size and generates unnecessarily large files. More on sizes in the next section.
sizes, quality, and the Image Optimization API
The sizes prop is the most underused optimization lever in the entire component. It maps to the HTML srcset sizes attribute and tells the browser which image variant to download based on viewport width. Get this wrong and you're serving 1200px images to mobile users.
Write it like a CSS media query list. The last value is the default. For example: sizes="(max-width: 480px) 100vw, (max-width: 1024px) 50vw, 33vw" means full-width on small phones, half-width on tablets, one-third on desktops. Next.js uses this to generate the right srcset entries at build or request time.
The quality prop defaults to 75. It's an integer from 1 to 100. In most cases 75 is fine — visually identical to 90+ for photography at screen resolutions. For screenshots or text-heavy images where sharpness matters, bump it to 85 or 90. Going above 90 rarely improves perceived quality but does increase file size noticeably.
If you're building a UI with lots of visual elements and want to understand how image performance interacts with your overall render budget, the React performance guide goes deeper on profiling strategies that work alongside Next.js.
priority, loading, and LCP Optimization
Add priority to any image that appears above the fold — hero images, article thumbnails in a list header, anything visible without scrolling. This tells Next.js to preload the image, skipping lazy loading entirely. It's the single biggest thing you can do for LCP on image-heavy pages.
Without priority, Next.js defaults to loading="lazy". That's correct for most images but disastrous for your hero. The browser won't start fetching it until the image enters the viewport, which on a first load means a significant LCP penalty.
Don't add priority to everything. It defeats the purpose. Lazy loading exists to avoid downloading images the user never sees. Preloading everything floods the network during page load and can actually slow down the critical path. One or two priority images per page is the norm.
Have you ever opened DevTools Network tab and seen 30 images load simultaneously on page load? That's what happens without lazy loading. You want the browser to focus bandwidth on what's visible first.
placeholder, blurDataURL, and Perceived Loading Speed
The placeholder prop accepts "blur" or "empty". With "empty" (the default), there's just blank space while the image loads. With "blur", a tiny blurred version shows up immediately, then transitions to the real image. It's a subtle UX improvement that makes the page feel faster even when it isn't.
For static imports, Next.js generates the blurDataURL automatically — no extra work. For dynamic src strings, you need to supply it yourself. The value is a base64-encoded tiny image, usually 10x10 pixels. You can generate these with the plaiceholder package at build time.
// Static import: blur placeholder is automatic
import cardImage from '@/public/card-bg.jpg'
export function Card() {
return (
<Image
src={cardImage}
alt="Card background"
width={400}
height={250}
placeholder="blur"
/>
)
}
// Dynamic src: you provide blurDataURL
export function DynamicCard({ src, blurUrl }: { src: string; blurUrl: string }) {
return (
<Image
src={src}
alt="Dynamic card"
width={400}
height={250}
placeholder="blur"
blurDataURL={blurUrl}
/>
)
}This pairs naturally with UI patterns that use glassmorphism or translucent overlays. If you're building cards with frosted-glass effects, check out what is glassmorphism — the blur placeholder visually ties in with that aesthetic nicely.
Configuring External Domains in next.config
External image URLs require explicit allow-listing in next.config.js. This is a security measure. Without it, anyone could craft a URL that routes arbitrary external images through your optimization API and costs you money.
The modern way uses remotePatterns — it replaced the old domains array as of Next.js 13 and is more expressive. You can match by protocol, hostname, port, and pathname pattern.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/photo-**',
},
{
protocol: 'https',
hostname: '**.your-cdn.com',
},
],
// Optional: override default generated sizes
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
module.exports = nextConfigThe deviceSizes and imageSizes arrays control what variants the optimizer generates. deviceSizes are for full-width or near-full-width images. imageSizes are for smaller UI elements. Together they define your srcset ladder. The defaults are usually fine, but if your design system uses specific breakpoints — say your grid shifts at 720px — you might add that to align optimization with your actual layout.
If you're combining this with a design system approach, take a look at Tailwind vs CSS Modules for context on how styling decisions interact with component-level image sizing.
When NOT to Use next/image
It's not the right tool for everything. SVGs should almost never go through next/image — they're already optimized vector files and the component adds processing overhead with zero benefit. Use a plain <img> or inline the SVG.
Open Graph and email images don't benefit from the component either. Those are referenced by external services that don't run JavaScript, so the lazy loading and blur placeholders are irrelevant. Generate static files for those and reference them directly.
Icons, favicons, CSS background images — same story. The component shines for photos, screenshots, and hero banners where file size and lazy loading genuinely matter. For everything else, it's extra complexity for no gain.
Also: if you're running a static export (output: 'export' in next.config), the default image optimization won't work because it requires a server. You'll need to set loader to a custom function pointing at a CDN like Cloudinary or Imgix, or use unoptimized: true on individual images. Build with React TypeScript tips in mind here — typing your custom loader correctly prevents subtle bugs at runtime.
FAQ
You're using an external URL without adding the hostname to remotePatterns in next.config.js. Add a matching pattern with the correct protocol and hostname. If you're using the old domains array, it still works in Next.js 15 but is deprecated — migrate to remotePatterns.
Yes, but the width and height props on the component control the intrinsic size used by the optimizer — they're not the same as the CSS display size. Use className with Tailwind utilities like w-full h-auto to control how it renders visually, while keeping the width/height props accurate to the source image dimensions.
priority adds a <link rel='preload'> tag in the document head so the browser fetches the image before it even parses the component. loading='eager' just disables lazy loading but doesn't preload. For above-the-fold LCP images, priority gives you a measurable head start.
You've probably passed an invalid base64 string or a URL instead of a base64 data URI. The format must be data:image/jpeg;base64,<base64string>. If you're generating it with the plaiceholder package, use the base64 output directly from getPlaiceholder() — don't pass the URL.
Absolutely. The component renders a <span> wrapper in some configurations, which can occasionally break flex child assumptions. If you hit alignment issues, wrap it in a <div> and apply your flex/grid styles to the wrapper rather than directly to the Image component.
Add the unoptimized prop directly: <Image src={src} unoptimized ... />. This bypasses the optimization API entirely for that image and serves the original file. Useful for GIFs (Next.js doesn't animate optimized GIFs) or when you're already serving pre-optimized images from a CDN.