EmpireUI
Get Pro
← Blog8 min read#next-js#image-optimization#react

Next.js Image Optimization: Every Setting and Its Trade-Off

Next.js Image component settings explained without fluff — formats, sizes, lazy loading, remote patterns, and the trade-offs every developer should know before shipping.

Code editor showing Next.js configuration with image optimization settings

Why the Default Next.js Image Settings Will Surprise You

Honestly, most developers ship <Image> from next/image assuming the defaults are production-ready. They're not bad — but they're conservative, and understanding what's actually happening under the hood will save you hours of debugging LCP regressions.

The next/image component was introduced in Next.js 10 and has gone through significant changes since. As of Next.js 14.2 and beyond, the component runs through an on-demand image optimization pipeline that serves WebP or AVIF depending on what the browser accepts. That sounds automatic and painless. It mostly is. But every automatic decision has a trade-off you should know about.

If you care about React performance in production, images are usually your biggest win. A poorly configured <Image> can balloon your LCP score from 1.2s to 4.8s without a single line of JavaScript to blame.

The `sizes` Prop: The One You're Almost Certainly Getting Wrong

The sizes prop tells the browser how wide the image will actually render at different viewport breakpoints. It doesn't control the image's CSS width — that's your layout's job. sizes is a hint to the browser's preload scanner so it can fetch the right resolution before your CSS even loads.

If you omit sizes on a non-fill image, Next.js defaults to 100vw for every breakpoint. That means on a 1440px desktop, the browser may fetch a 1440px-wide image for something that renders at 320px. You're handing out 4× the data for free.

Here's a pattern that actually works for a three-column card grid that collapses to full-width on mobile: ``tsx <Image src="/products/card-hero.jpg" alt="Product hero" width={480} height={320} sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" className="rounded-xl object-cover" /> ` That one sizes` string can cut image payload by 60% on mobile without any visible quality loss.

AVIF vs WebP: Format Trade-Offs in `next.config.js`

By default in Next.js 13+, the optimizer attempts AVIF first, then falls back to WebP, then to the original format. AVIF consistently beats WebP on compression — usually 20–30% smaller files at equivalent quality. The catch: AVIF encoding is CPU-intensive. On a cold server with no cached variants, encoding a 2MB source image to AVIF can take 3–8 seconds.

For most SaaS products with predictable image sets, that cold-encode cost is a one-time hit. The cached variant is served instantly after. But if you're running an ecommerce catalog where new product images arrive daily, you'll feel those encoding spikes in your server response times.

You can override the format priority in next.config.js: ``js // next.config.js module.exports = { images: { formats: ['image/webp'], // drop AVIF if encoding latency hurts you minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days deviceSizes: [640, 750, 828, 1080, 1200, 1920], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, }; ` Drop AVIF from formats` if your server CPU is constrained. Keep it if you're on a CDN with edge workers handling the encoding.

Remote Patterns: Locking Down External Image Sources

Before Next.js 12.3, you'd whitelist external hostnames with the domains array. That API still works but it's deprecated in favor of remotePatterns, which is far more granular. domains allows any path and protocol from a hostname. remotePatterns lets you lock down the protocol, hostname, port, and pathname glob independently.

Why does this matter? A misconfigured domains entry means someone can craft a URL like /_next/image?url=https://evil.com/tracking-pixel.gif&w=1&q=1 and your server happily proxies and caches external content. remotePatterns with a strict pathname keeps that attack surface small.

``js // next.config.js — restrict Cloudinary to your own account module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'res.cloudinary.com', pathname: '/your-account-id/**', }, { protocol: 'https', hostname: 'images.unsplash.com', pathname: '/photo-**', }, ], }, }; `` This kind of explicit allow-list is the sort of thing that gets caught in a security review six months after launch. Better to do it now.

Lazy Loading, Priority, and LCP Images

Every <Image> without priority gets loading="lazy" applied automatically. That's the right default for below-the-fold images. But it's the wrong behavior for your hero image, your above-the-fold product shot, or anything that contributes to your LCP score.

Add priority to the one or two images a user sees immediately on page load. Don't add it to everything — you'll defeat the purpose. priority injects a <link rel="preload"> tag in the document head, which tells the browser to fetch the image as early as possible, even before it parses the component tree. That can shave 400–900ms off your LCP on slow connections.

What about blur placeholders? The placeholder="blur" prop works beautifully for local images — Next.js generates a tiny base64 data URL at build time. For remote images, you need to supply your own blurDataURL. A 10×10px JPEG encoded as base64 is under 200 bytes and gives you a smooth fade-in without layout shift. Worth the small effort, especially if you're building glassmorphism-style UIs where empty image slots look jarring.

Custom Loaders: When You're Not Using Vercel's CDN

Vercel's default image optimization is free up to 1,000 source images and 5,000 optimized images per month on the Hobby plan. Once you're past that, you're paying per image or you want to run your own pipeline. That's where custom loaders come in.

A loader is just a function that takes { src, width, quality } and returns a URL string. You can point it at Cloudinary, Imgix, Fastly, or your own sharp-powered Node server. The component handles everything else — responsive srcset, lazy loading, aspect ratio — your loader just constructs the right URL.

``tsx // lib/imageLoader.ts import { ImageLoaderProps } from 'next/image'; export function cloudinaryLoader({ src, width, quality }: ImageLoaderProps): string { const q = quality ?? 75; return https://res.cloudinary.com/your-account/image/upload/w_${width},q_${q},f_auto/${src}; } // Usage import { cloudinaryLoader } from '@/lib/imageLoader'; <Image loader={cloudinaryLoader} src="products/sneaker-hero.jpg" width={800} height={600} alt="Premium sneaker product shot" sizes="(max-width: 768px) 100vw, 50vw" /> ` Set the default loader globally in next.config.js via loader: 'custom' and loaderFile: './lib/imageLoader.ts'` if you want it applied across the project without per-component plumbing.

deviceSizes and imageSizes: The Two Arrays Nobody Reads

Next.js generates responsive image variants using two arrays: deviceSizes (used for full-viewport images) and imageSizes (used for fixed-width or smaller components). The defaults are [640, 750, 828, 1080, 1200, 1920, 2048, 3840] for deviceSizes and [16, 32, 48, 64, 96, 128, 256, 384] for imageSizes.

The combined array gets sorted and deduplicated — every value becomes a possible w= parameter in your /_next/image URL. More values means more cache variants and more disk/memory usage on your image server. Fewer values means fewer cache hits per unique request dimension. You want the sweet spot that matches your actual design breakpoints.

If your app's widest layout column is 1200px and your card thumbnails never exceed 384px, you don't need the 2048 and 3840 entries. Trim the arrays to your actual design system. Honestly, auditing these two arrays is one of the first things I do when joining a new project — it's almost always bloated. For apps using Tailwind vs CSS Modules approaches to layout, your breakpoints are already well-defined, so matching these arrays to your Tailwind config breakpoints makes perfect sense.

Measuring the Impact: Web Vitals You Should Track

All of this config work means nothing if you're not measuring. The metrics that matter for images are LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), and total image bytes transferred. Chrome DevTools' Performance panel, Lighthouse, and the Web Vitals library all surface these.

CLS from images happens when the browser doesn't know the image's dimensions before it loads and reserves no space for it. The width and height props on <Image> aren't just for aspect ratio hints — they prevent layout shift entirely by setting an intrinsic size the browser respects before the image arrives. Never omit them unless you're using fill mode with a positioned container.

Want to see real numbers? Add import { onLCP, onCLS } from 'web-vitals' to your _app.tsx and log to your analytics. A hero image without priority often shows up as the LCP element with a 2–3s delay on a throttled 3G connection. Fixing that with one prop addition is one of those rare wins that takes 30 seconds and shows up immediately in field data. If you're building component-heavy apps, combining this with the patterns in our React performance guide gives you a solid baseline.

FAQ

Can I use `next/image` with a static export (`output: 'export'`)?

Yes, but you lose server-side optimization. With output: 'export', you must either use unoptimized: true globally or provide a custom loader that points to an external image CDN like Cloudinary or Imgix. The built-in optimization pipeline requires a running Node server.

What's the difference between `width`/`height` props and the `sizes` prop?

width and height define the image's intrinsic dimensions — they prevent CLS and set the aspect ratio. sizes is a media-condition string that tells the browser how wide the rendered image will be at various viewport widths, so it can pick the right resolution from the srcset. They serve completely different purposes and you need both.

How do I handle images from a CMS where I don't know dimensions at build time?

Use the fill prop with a relatively-positioned parent container. Set the parent's dimensions via CSS (e.g., style={{ position: 'relative', height: '300px' }}), then add object-fit via className. You lose the CLS protection of intrinsic dimensions, but the parent container's known height compensates for it.

Is `minimumCacheTTL` the same as browser cache headers?

No. minimumCacheTTL (in seconds) controls how long Next.js caches the optimized image variant on the server side before it's eligible for re-optimization. Browser cache is controlled separately via Cache-Control headers. Setting a long minimumCacheTTL (e.g., 2592000 for 30 days) reduces server CPU but means image updates won't propagate until the TTL expires.

Why does my image look blurry even with high `quality` values?

The quality prop (1–100, default 75) controls lossy compression. But blurriness often comes from a mismatched sizes prop — if the browser fetches a 640px variant for a 1280px display slot, you'll see blur. Check your Network tab for the actual w= parameter being requested and compare it to the rendered image width in DevTools.

Does the `priority` prop actually add a `<link rel="preload">`?

Yes. When you set priority, Next.js injects a <link rel="preload" as="image" imagesrcset="..." imagesizes="..."> tag into the document <head>. This triggers a browser preload fetch before the component hydrates, which is why it can dramatically cut LCP. Only use it on images that are genuinely above the fold.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js Performance Checklist 2026: Every Optimization, RankedNext.js Image Component Deep Dive: All Props, Performance ImpactCore Web Vitals in 2026: LCP, INP, CLS with Real Next.js FixesLighthouse CI: Automated Performance Checks in GitHub Actions