Next.js 16 Image Optimisation: A Practical Guide
The next/image component can cut image weight dramatically, but only if you give it the right information. The same few problems cause most slow image loading on Next.js sites: a missing sizes prop, a lazy-loaded hero image, or settings that were right in an older version of Next.js. This guide explains how the component actually works in Next.js 16 and how to avoid those mistakes.
How next/image works
When you render <Image>, Next.js outputs a normal <img> with a srcset: a list of URLs for the same image at different widths, all served by the built-in optimiser at /_next/image.
- The browser chooses the width. Using the
sizesattribute and the screen's pixel density, it picks the smallest candidate fromsrcsetthat will look sharp. - The optimiser resizes on request. The first time a width is requested, Next.js resizes and compresses the source image, then caches the result.
- The format follows the browser. Next.js reads the request's
Acceptheader and serves WebP by default. AVIF is available, but you have to turn it on (see below). - Space is reserved. Because the component knows the image's aspect ratio, the browser reserves the right amount of space before the file arrives, so text does not jump around. That protects your Cumulative Layout Shift (CLS) score.
The basics: dimensions or a static import
Every image needs src and alt, and Next.js needs to know its size. The easiest way is a static import, which reads the dimensions from the file and generates a blur placeholder for you:
import Image from "next/image";
import team from "@/public/team.jpg";
export function Team() {
return <Image src={team} alt="The Debuggers team at work" placeholder="blur" />;
}
For images you cannot import, such as files from a CMS, pass width and height in pixels. They set the aspect ratio and the largest size requested, not the size on screen, which you control with CSS:
<Image
src="https://cdn.example.com/products/chair.jpg"
alt="Oak dining chair"
width={1200}
height={900}
style={{ width: "100%", height: "auto" }}
sizes="(max-width: 768px) 100vw, 50vw"
/>
Get sizes right
sizes tells the browser how wide the image will be at different screen widths, so it can choose the right file from srcset. The Next.js docs are blunt about what happens without it: the browser assumes the image is as wide as the viewport (100vw) and can download a much larger file than necessary.
A product grid that shows one column on phones, two on tablets and four on desktops looks like this:
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
The values do not have to be exact. Rounding up slightly is fine; leaving sizes out is the expensive mistake.
Fill mode for flexible layouts
When a container's size comes from CSS (a card, a hero, an aspect-ratio box), use fill instead of width and height. The image is absolutely positioned to fill its nearest positioned parent, so that parent needs position: relative, absolute or fixed, and a height:
<div className="relative aspect-video w-full overflow-hidden rounded-xl">
<Image
src="/hero.jpg"
alt="Dashboard on a laptop"
fill
sizes="(max-width: 1024px) 100vw, 60vw"
style={{ objectFit: "cover" }}
/>
</div>
sizes matters even more with fill, because without it Next.js generates candidates all the way up to full viewport width.
The LCP image: preload, eager or high priority
Images are lazy-loaded by default, which is right for everything below the fold and wrong for the one image that is usually your Largest Contentful Paint (LCP): the hero.
In Next.js 16 the priority prop is deprecated and replaced by preload, which inserts a <link rel="preload"> for the image in the page head. The Next.js docs recommend a lighter touch in most cases:
// Usually enough: load straight away with high network priority
<Image src="/hero.jpg" alt="..." fill sizes="100vw" loading="eager" fetchPriority="high" />
// When the image must start downloading from the <head>
<Image src="/hero.jpg" alt="..." fill sizes="100vw" preload />
Use this on one image per page. Marking several images as high priority makes them compete for bandwidth and usually makes LCP worse. And do not preload an image that only appears on some screen sizes, because every visitor will download it.
Remote images and remotePatterns
The optimiser only fetches remote images from hosts you allow. If a src points at a host that is not configured, next/image throws an error when the page renders, telling you the hostname is not configured under images. Allow hosts as narrowly as you can in next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com", pathname: "/products/**" },
// Since Next.js 15.3 you can also pass URL objects:
new URL("https://images.example-cms.com/uploads/**"),
],
},
};
export default nextConfig;
The older images.domains option has been deprecated since Next.js 14. Replace it with remotePatterns, which also lets you restrict the path.
Turning on AVIF
WebP is the default. AVIF files are typically about 20% smaller than WebP, but they take roughly 50% longer to encode, and each format is cached separately. For image-heavy pages where the same images are requested often, it is usually worth it:
images: {
formats: ["image/avif", "image/webp"],
},
The order matters: Next.js serves the first format in the list that the browser accepts. If you put a CDN or proxy in front of a self-hosted Next.js app, make sure it forwards the Accept header, or every visitor gets the same format.
What changed in Next.js 16
If you are upgrading, check these image settings:
| Change | What to do |
|---|---|
priority deprecated | Use loading="eager" with fetchPriority="high", or preload |
qualities is now required and defaults to [75] | If you use quality={90} or similar, add it: qualities: [75, 90]. Other values snap to the nearest allowed one |
minimumCacheTTL now defaults to 4 hours | Raise it (for example to 31 days) if your images rarely change and you want fewer re-optimisations |
New maximumRedirects and dangerouslyAllowLocalIP options | Defaults are safe; only change them if you know why |
The qualities change is a security measure: without an allowlist, anyone could request many quality variants of your images and use up your optimisation budget.
SVGs, GIFs and tiny images
Some images gain nothing from optimisation. Next.js does not optimise SVGs and automatically treats a src ending in .svg as unoptimized. The docs also suggest unoptimized for very small images (under about 1 KB) and animated GIFs. For logos and icons, a plain <img> or an SVG component is often simpler.
Static exports and image CDNs
The built-in optimiser runs on the server, so it is not available with output: "export". You have two options:
- Set
images: { unoptimized: true }and optimise images yourself before deploying. - Use a custom loader that points at an image CDN such as Cloudinary or Imgix, which does the resizing instead.
A loader is a small function that turns src, width and quality into a URL:
// image-loader.ts
export default 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}`;
}
Then point images.loader: "custom" and images.loaderFile: "./image-loader.ts" at it in next.config.ts.
Common mistakes
- No
sizeson responsive orfillimages. The single biggest cause of oversized downloads. - Lazy-loading the hero. The LCP image should be eager with high priority.
- Too many "priority" images. One per page is the rule.
- Tiny
widthandheighton large displays. Those values cap the largest file requested, so awidth={400}image shown 1,200px wide on a retina screen looks blurry. - Styling the image instead of its container. For
fillimages, put rounded corners, aspect ratio and borders on the wrapper and useobjectFiton the image. - Uncompressed originals in
/public. The optimiser still has to read them, which slows the first request. Resize very large originals before committing them.
Checking the results
Run the page through PageSpeed Insights on mobile. The image delivery insight (older Lighthouse versions split it into "Properly size images" and "Serve images in next-gen formats") lists every image that is larger than it needs to be. In Chrome DevTools, the Network panel filtered to Img shows the width each /_next/image request asked for (the w= parameter) and the format actually served.
If a request's w= value is far above the displayed size, the sizes prop is the first thing to check.
Related resources
- Web performance and Core Web Vitals
- Shrink very large originals before uploading with our Image Resizer.
- Convert images to modern formats in your browser with our WebP Converter.
Frequently Asked Questions
Is the priority prop deprecated in Next.js 16?
Yes. Next.js 16 deprecates priority in favour of preload, which adds a preload link for the image. For most hero images the Next.js docs now suggest loading="eager" or fetchPriority="high" instead, and reserving preload for cases where the image must start downloading from the document head.
Can I use next/image with Cloudinary or another image CDN?
Yes. Set a custom loader, either per image with the loader prop or for the whole app with images.loader and images.loaderFile in next.config. The loader builds the CDN URL for each width, and Next.js skips its own optimiser.
Should I use next/image for SVG files?
You can, but it adds little. Next.js does not optimise SVGs and automatically treats a src ending in .svg as unoptimized. For icons and logos, a plain img tag or an SVG imported as a component is simpler.
Does next/image work with a static export?
The built-in optimiser needs a server, so with output: 'export' you either set images.unoptimized to true or configure a custom loader that points at an image CDN.
Why are my optimised images still large?
The most common cause is a missing or wrong sizes prop on responsive or fill images. Without it the browser assumes the image is as wide as the viewport and downloads a much larger file than it displays.
Found this helpful?
Join thousands of developers using our tools to write better code, faster.