Next.js Image Optimization and Performance
How the Next.js Image component handles optimization automatically, and the mistakes that quietly hurt LCP and CLS.
Images are the single biggest lever most teams have over their Core Web Vitals, and also the one most commonly mishandled. I’ve reviewed plenty of Next.js codebases where every image on the page uses a plain <img> tag, the hero image loads lazily by accident, and nobody can explain why their Largest Contentful Paint score refuses to improve. The next/image component solves almost all of this automatically — but only if you understand what it’s actually doing under the hood.
What next/image Does For You
The Image component is a wrapper around the native <img> element that adds automatic resizing, modern format conversion (WebP/AVIF where supported), lazy loading by default, and built-in protection against layout shift.
import Image from 'next/image';
import heroImage from '@/public/images/hero.jpg';
export function Hero() {
return (
<Image
src={heroImage}
alt="Team collaborating around a laptop"
priority
/>
);
}
When you import a local image file like this, Next.js reads its actual dimensions at build time and generates the correctly sized width and height attributes automatically — that’s why you don’t have to specify them yourself for static imports. For remote images, you do need to supply width and height explicitly (or use fill), since Next.js has no way to inspect a remote file’s dimensions ahead of time.
<Image
src="https://cdn.example.com/products/sneaker.jpg"
alt="Running sneaker, side profile"
width={600}
height={400}
/>
Behind the scenes, Next.js serves these through an image optimization endpoint (or a configured loader for external CDNs) that resizes and re-encodes the image on demand, caching the result. The browser only downloads the size and format it actually needs for the current viewport — a phone doesn’t download the same 2000px-wide asset a desktop does.
priority and Largest Contentful Paint
By default, every Image is lazy-loaded — it doesn’t start downloading until it’s near the viewport. That’s the right default for most images on a page, but it’s actively harmful for whichever image is likely to be your LCP element (almost always a hero image, banner, or above-the-fold product photo).
// Hero image — this is almost certainly the LCP candidate
<Image
src={heroImage}
alt="Product launch banner"
priority
/>
priority does two things: it disables lazy loading (so the browser starts fetching immediately) and adds a fetchpriority="high" hint, nudging the browser to prioritize this request over other same-priority resources. Without it, the browser might not even discover the image URL until well after the initial HTML parse — directly delaying LCP. Only mark the actual above-the-fold hero or primary content image as priority; adding it to several images defeats the purpose, since the whole point is telling the browser what to prioritize first.
Avoiding Layout Shift with width/height or fill
Cumulative Layout Shift happens when an image loads after the surrounding layout has already been painted and pushes content around. The fix is reserving the image’s space in the layout before it loads, which is exactly what width and height (or fill with a sized parent) do — Next.js uses them to compute an aspect ratio and reserve that space up front.
// Fixed dimensions known ahead of time
<Image src={productImage} alt="Wireless headphones" width={400} height={400} />
When you don’t know the exact pixel dimensions ahead of time — a card in a responsive grid, say — use fill with a positioned, explicitly sized container instead:
<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 9' }}>
<Image
src={bannerImage}
alt="Seasonal sale banner"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
</div>
fill makes the image absolutely positioned to cover its parent, so the parent is responsible for defining the aspect ratio — forgetting to size the container is the most common bug with fill, and it usually results in either a collapsed zero-height image or one that stretches to fill unrelated space.
The sizes Prop Matters More Than People Think
sizes tells the browser how wide the image will actually be rendered at different viewport widths, so it can pick the right file from the responsive srcset Next.js generates. Omitting sizes on a fill image (or on a responsive layout image) causes the browser to fall back to assuming the image is as wide as the viewport, which means it may download a much larger file than it needs on a narrow mobile screen.
<Image
src={productImage}
alt="Product thumbnail"
fill
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
Get this roughly right — it doesn’t need pixel-perfect precision, just an honest approximation of the rendered width at each breakpoint — and you avoid shipping desktop-sized images to phones.
Common Mistakes
Not setting priority on the hero image. This is the single most common LCP regression I see in Next.js audits. If the biggest visible element on first paint is an Image without priority, you’re very likely leaving LCP improvement on the table for free.
Using fill without a sized, positioned container. fill images need position: relative (or similar) on the parent with an explicit height or aspect ratio — otherwise the image collapses or overflows unpredictably.
Importing huge unoptimized source images and assuming the component “handles it.” next/image optimizes the delivered file, but it still has to read the original. A 20MB uncompressed PNG checked into the repo adds real time to builds and dev-server cold starts. Compress and right-size your original source assets before importing them, even though the delivered output will be resized regardless.
Setting priority on too many images. If everything is marked high priority, nothing effectively is — the browser’s prioritization only helps when it’s reserved for the one or two images that actually gate the page’s perceived load.
Using a plain <img> tag for content images “to keep it simple.” This opts out of resizing, format conversion, and lazy loading entirely, and it’s a common source of CLS when no explicit dimensions are set manually.
When NOT to Rely on next/image
For truly decorative background images applied purely via CSS (background-image), the Image component doesn’t apply — use standard CSS and handle responsive variants with image-set() or media queries instead. And if you’re using a custom CDN/image provider, you’ll need to configure a custom loader (images.loaderFile in next.config.js) since the default optimizer assumes either local files or a configured set of allowed remote patterns.
Image performance is one piece of a bigger Core Web Vitals picture — rendering strategy affects how early in the request lifecycle that hero image can even start loading, which is covered in Next.js SSR vs SSG vs ISR Explained. For the broader set of techniques beyond images — bundle size, font loading, critical rendering path — see Frontend Performance Optimization Techniques. And if you’re structuring a larger app where image-heavy components need to stay maintainable across teams, Scalable Frontend Architecture covers the broader organizational patterns.
FAQ
Does next/image work with external image URLs out of the box?
No — you need to allowlist the remote domain in next.config.js under images.remotePatterns, otherwise Next.js refuses to optimize it as a security measure against arbitrary remote fetches.
Does lazy loading hurt SEO since the image isn’t in the initial HTML?
No — the img tag (including its src/srcset) is present in the server-rendered HTML either way; loading="lazy" only affects when the browser fetches the resource, not whether it’s in the document for crawlers to see.
Can I disable image optimization entirely?
Yes, with unoptimized on the component or images.unoptimized: true globally — sometimes necessary for static exports without an image optimization server available, though you lose automatic resizing and format conversion.
Related reading
Written by
Tayyab Roy
Senior Frontend / Full-Stack Engineer
Senior Frontend & Full-Stack Engineer specializing in Angular, React, Next.js and enterprise eCommerce (Shopify, SAP Commerce / Hybris, Spartacus). I write about practical, production-tested frontend engineering.
Related Articles
Next.js SSR vs SSG vs ISR Explained
Server-side rendering, static generation, and incremental static regeneration in Next.js — what each is for and how to choose.
Next.js Server Components vs Client Components
A clear mental model for Next.js Server Components vs Client Components — what runs where, and how to decide.
Frontend Performance Optimization Techniques
A framework-agnostic checklist of techniques that move the needle on Core Web Vitals — LCP, CLS, and INP.