Headline: Using
next/imageis not the same as having fast images. The component lazy-loads every image it renders unless you passpriority, and a wrongsizesprop makes a 400-pixel-wide card download the 3840-pixel candidate on a high-DPR screen.
next/image is the Next.js built-in component that generates a responsive srcset, converts source files to modern formats on demand, and reserves layout space before a byte arrives. I have used it on every Next.js project I have built, and I still spent an afternoon this month working out why a page full of <Image> tags had a worse Largest Contentful Paint than the plain <img> markup it replaced. The layout-shift half is automatic. The bandwidth and latency half is not.
Key takeaways
-
next/imagelazy-loads every image by default, including the one that is your Largest Contentful Paint element. Only thepriorityprop opts an image out of lazy loading and adds a preload hint. - The
sizesprop tells the browser how wide the image will render before CSS is applied. An<Image fill>with nosizesis treated as100vw, which selects the largest candidate in the srcset. - Next.js 16 removed the
images.domainsoption, soimages.remotePatternsis the only way to allow a remote host. - Next.js 16 restricts the
qualityprop to values listed inimages.qualities, which defaults to[75]. - An image transformation is a unique combination of source image, width, quality and output format, so a wide
deviceSizesarray multiplies both cost and cache misses.
Why is my LCP still slow when I already use next/image?
Because next/image lazy-loads every image by default, including the one that is your Largest Contentful Paint element. Largest Contentful Paint measures when the biggest visible element finishes rendering. A lazy image is not requested until the browser has run layout and decided the image is near the viewport, so the preload scanner — which normally starts image downloads while the HTML is still being parsed — never sees it.
The fix is one prop, applied to exactly one image per route:
import Image from 'next/image';
import hero from '@/public/hero.jpg';
export default function Hero() {
return (
<Image
src={hero} // static import: width, height and blurDataURL come for free
alt=""
priority // no lazy loading, fetchpriority="high", preload hint in head
sizes="100vw"
className="w-full h-auto"
/>
);
}
The priority prop does three things: it removes loading="lazy", sets fetchpriority="high", and emits a preload link in the document head. Marking six images priority is the same as marking none, because six high-priority requests then compete for the same connection.
Two related traps cost me time. First, placeholder="blur" inlines a base64 data URI into the HTML, so a heavy blurDataURL grows the document on the critical path. Second, an image rendered by a client component that only mounts after hydration cannot be preloaded at all, whatever you pass to priority — the markup does not exist when the preload scanner runs.
What does the sizes prop actually do, and when does it double my bandwidth?
The sizes attribute tells the browser how wide the image will be rendered, so it can pick a srcset candidate before stylesheets are applied. Choose the candidate list badly and the browser downloads the biggest file you offered it.
Without sizes, Next.js emits a fixed 1x/2x srcset built from the width you passed. With sizes, it emits a full candidate list drawn from images.deviceSizes (default 640, 750, 828, 1080, 1200, 1920, 2048, 3840) and images.imageSizes (default 16, 32, 48, 64, 96, 128, 256, 384). The fill prop with no sizes is treated as 100vw, so on a 1920-pixel display at device pixel ratio 2 the browser asks for the 3840-pixel file — for a card that renders at 400 pixels.
// A product card image that is never wider than 400 CSS pixels.
<Image
src={product.image}
alt={product.name}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
/>
The rule I now apply in review: an <Image fill> with no sizes prop is a defect, not a style preference. Verify it in the Chrome DevTools Network panel by comparing the transferred size of each image against the box it renders into.
Should I use fill or explicit width and height?
Use explicit width and height whenever you know the intrinsic dimensions, because Next.js turns them into a CSS aspect-ratio that reserves space and keeps Cumulative Layout Shift at zero. A static import supplies both dimensions and a generated blurDataURL at build time, so it is the cheapest correct option for any asset in your repository.
Reach for fill only when the rendered box is decided by CSS and the source aspect ratio varies — user-uploaded avatars, CMS hero images, a masonry grid. fill absolutely positions the image, so the parent needs position: relative and a non-zero height, and cropping is your job through object-fit. Every fill image also needs a sizes prop.
What changed for images in Next.js 16?
Next.js 16 tightened image configuration in three ways that break existing config files. The images.domains option was removed in favour of images.remotePatterns. The quality prop is now restricted to values listed in images.qualities, which defaults to [75]. And images.localPatterns lets you restrict which local paths the optimizer will accept, which matters because /_next/image is a public endpoint that anyone can call with arbitrary parameters.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
// images.domains was removed in Next.js 16. remotePatterns is the only form.
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com', pathname: '/products/**' },
],
// Only these quality values are accepted. Anything else is rejected.
qualities: [50, 75],
localPatterns: [{ pathname: '/assets/**', search: '' }],
formats: ['image/avif', 'image/webp'],
// Set this explicitly. A short TTL recomputes transformations you already paid for.
minimumCacheTTL: 60 * 60 * 24 * 31,
},
};
export default nextConfig;
SVG is still refused by the optimizer unless you set dangerouslyAllowSVG: true, and that default is correct: an SVG is an executable document, and optimizing one from an untrusted host turns your own origin into the delivery vehicle for its scripts.
Should images.formats list AVIF or WebP first?
List AVIF first when bandwidth is the constraint, and WebP first when first-request latency is. The images.formats array is ordered by preference, and the optimizer picks the first entry the requesting browser accepts.
| Format | File size | Encode cost | Reach for it when |
|---|---|---|---|
| AVIF | Smaller than WebP at equal visual quality | Noticeably slower to encode | Images are cached and served many times; bytes dominate |
| WebP | Larger than AVIF, far smaller than JPEG | Fast | Long-tail images with few hits, where every request is a cache miss |
| JPEG/PNG source | Largest | None | Fallback only, for clients that accept neither |
The trade-off is uneven across your site. On a marketing page with five hero images that every visitor loads, AVIF encoding happens once and the smaller bytes win forever. On a catalogue with fifty thousand product photos where most are viewed once, the slower encode is on the critical path of a real user every time.
How do I keep image transformation cost bounded?
An image transformation is a unique combination of source image, requested width, quality and output format, and each unique combination is computed and billed once before it is cached. That definition is the whole cost model: everything that multiplies the number of distinct combinations multiplies your bill.
-
Trim
deviceSizes. Eight default widths times two formats is sixteen possible transformations per source image. If your layout has three real breakpoints, list three widths. - Use one quality value. Next.js 16 already forces you to declare them; declare one.
-
Raise
minimumCacheTTL. Next.js honours an upstreamCache-Controlmax-age when it is longer thanminimumCacheTTL, so a CMS that sendsno-storequietly defeats the cache. -
Set
unoptimizedon assets that are already optimized — sprite sheets, small PNG icons, anything your build pipeline has already squeezed. - Watch for cache-busting query strings. A source URL with a changing token is a new source image every time, and therefore a new transformation every time.
When should I bypass next/image entirely?
Three cases justify leaving the component behind. For art direction — a different crop on mobile than desktop — call getImageProps() (stable since Next.js 15, previously unstable_getImgProps) to obtain the generated srcset and feed it into your own <picture> element or a CSS background. For a static export (output: 'export') there is no server to run the optimizer, so you must set images.unoptimized: true or supply a custom loader. And if you already pay for an image CDN such as Cloudinary or imgix, point images.loaderFile at it rather than optimizing twice.
Self-hosting has one more requirement worth stating plainly: the built-in optimizer needs the sharp package installed. The pure-JavaScript fallback was removed in earlier releases, so a self-hosted deployment without sharp will fail to optimize rather than silently degrade.
FAQ
Q: Should I add priority to every above-the-fold image?
A: No. Add priority to the single image most likely to be the Largest Contentful Paint element. Multiple high-priority images compete for bandwidth and delay the one that actually determines the metric.
Q: Why does my optimized image look soft on a Retina screen?
A: Next.js never upscales beyond the intrinsic size of the source file. If the browser requests a 1600-pixel candidate and the source is 800 pixels wide, you get 800 pixels rendered into a 1600-pixel box. Replace the source asset; no configuration fixes it.
Q: Do I need sharp when self-hosting Next.js?
A: Yes. The built-in image optimizer requires the sharp package outside of Vercel, where the platform provides its own optimization layer.
Q: Does next/image work with output: 'export'?
A: Not with the default loader, because a static export has no server. Set images.unoptimized: true to emit plain <img> tags, or configure images.loaderFile to point at an external image CDN.
Q: Is AVIF always the better choice?
A: No. AVIF produces smaller files than WebP at comparable quality but takes measurably longer to encode, so on images with low cache-hit rates the encode time lands on a real user's first request.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.
Top comments (0)