Next.js Image Component Optimization
The next/image component is the most widely deployed image optimizer on the web, and also the most widely misconfigured. It is part of Framework & Build-Tool Media Integration, and it packs the whole responsive pipeline — resizing, format negotiation, srcset generation, layout reservation, and priority hinting — behind a single <Image> tag. This guide is a close reading of what that tag actually emits, how the built-in /_next/image optimizer works, and every configuration knob that changes its output: images.formats, deviceSizes, imageSizes, sizes, fill, priority, placeholder, remotePatterns, quality, and unoptimized.
How the built-in optimizer works
When you render <Image src="/hero.jpg" width={1200} height={630} />, Next.js does not put /hero.jpg in the src. It rewrites the URL to its optimization endpoint:
/_next/image?url=%2Fhero.jpg&w=1200&q=75
That endpoint is a request handler backed by sharp. On the first request for a given url+w+q triple it decodes the source, resizes to w, re-encodes to the best format the client’s Accept header supports (per images.formats), and streams the result with a long-lived Cache-Control. Subsequent requests for the same triple are served from the optimizer’s on-disk cache (.next/cache/images by default). The component builds a srcset by repeating this URL across the applicable widths, so the browser can pick a candidate — the optimizer resizes on demand for each width the browser actually requests.
Warning: the on-disk optimizer cache is keyed by url+w+q+Accept. On a serverless deploy where each cold invocation starts with an empty cache, the first visitor to each variant pays the full sharp encode latency. Warming the cache or fronting /_next/image with a durable CDN cache is essential for consistent LCP.
Configuration reference
Everything the optimizer does is governed by the images block in next.config.js.
| Config key | Purpose | Default / note |
|---|---|---|
images.formats |
Preferred output formats in priority order | ['image/webp']; add 'image/avif' first for AVIF |
images.deviceSizes |
Widths used when sizes implies viewport-relative rendering |
[640,750,828,1080,1200,1920,2048,3840] |
images.imageSizes |
Extra small widths for fixed-size images (added to deviceSizes) | [16,32,48,64,96,128,256,384] |
images.qualities |
Allowed quality values (Next 15+ allowlist) |
must include any quality prop you pass |
images.remotePatterns |
Allowlist of remote hosts the optimizer may fetch | empty; required for any external src |
images.minimumCacheTTL |
Floor for the optimizer’s cache lifetime (seconds) | 60; raise for stable assets |
images.loader / loaderFile |
Swap the built-in optimizer for a custom/CDN loader | 'default' |
images.unoptimized |
Bypass the optimizer entirely (emit raw src) |
false |
images.dangerouslyAllowSVG |
Permit SVG through the optimizer | false (SVG is a script vector) |
Setting AVIF output
// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
images: {
// Order matters: the optimizer tries formats left-to-right against the
// request's Accept header and serves the FIRST match. Listing AVIF first
// means Chrome/Firefox/Safari-16 get AVIF; Safari 14/15 fall through to WebP.
formats: ['image/avif', 'image/webp'],
// AVIF encodes ~20% smaller than WebP here but costs materially more CPU
// per resize. On a serverless optimizer that raises cold-path latency —
// budget for it or offload to a CDN loader.
minimumCacheTTL: 2678400, // 31 days — keep optimized variants around longer
},
};
How deviceSizes and imageSizes shape the srcset
The two width lists serve different image kinds, and understanding which one applies prevents both blurry images and wasteful over-fetching.
deviceSizesis used when the image is viewport-relative — that is, when yoursizescontains a viewport unit like100vwor50vw. Next multiplies each device size against the layout to build thesrcset, so these values should map to the common device widths and DPRs in your audience.imageSizesis used when the image is a fixed size (asizesof240px, an avatar, an icon). These small widths are appended to the candidate pool so a 48px avatar does not have to download a 640px file.imageSizesvalues are only ever used withdeviceSizes, never alone.
Concretely, an image with sizes="(max-width: 768px) 100vw, 384px" produces a srcset that pulls the near-384 entries for desktop and the wider deviceSizes entries for the full-bleed mobile case:
/_next/image?url=%2Fcard.jpg&w=640&q=75 640w,
/_next/image?url=%2Fcard.jpg&w=750&q=75 750w,
/_next/image?url=%2Fcard.jpg&w=828&q=75 828w, …
Tradeoff: every entry in deviceSizes is a distinct variant the optimizer may encode and cache. The default eight-width list is generous; trimming it to the four or five widths your breakpoints actually select cuts optimizer CPU, cache footprint, and (on a metered image CDN loader) transform cost — at the price of slightly coarser DPR matching on unusual screens.
Step-by-step: a correct next/image setup
Step 1 — Import local images so dimensions are known
// Static import → Next reads intrinsic width/height at build time and derives
// blurDataURL. This is what lets it reserve layout space and avoid CLS with
// zero extra props.
import Image from 'next/image';
import hero from '@/assets/hero.jpg';
export function Hero() {
return (
<Image
src={hero}
alt="Warehouse floor with autonomous forklifts"
priority // LCP: fetchpriority=high + preload link
placeholder="blur" // uses the auto-derived blurDataURL
sizes="(max-width: 640px) 100vw, 640px"
/>
);
}
Step 2 — Set sizes to match the real layout
The optimizer generates srcset widths from deviceSizes, but it needs sizes to know which one the browser should pick. Omit sizes on a responsive image and Next defaults to 100vw, which on a 4K display selects the 3840 variant for a 600px column — a multi-hundred-kilobyte over-fetch.
// A three-column grid image: full width on phones, half on tablets,
// one-third of a 1200px max container on desktop (~380px).
<Image
src={product}
alt="Product thumbnail"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 380px"
/>
Deriving these percentages correctly is a topic of its own — see how to calculate optimal sizes attribute values.
Step 3 — Choose fill or fixed layout
For a fixed, known display size, pass width/height (or use the static import, which supplies them). For a container-driven image that must cover an arbitrary box, use fill:
// fill: the image becomes position:absolute; inset:0; object-fit:cover.
// The PARENT must be positioned and sized, or the image collapses to 0px.
<div style={{ position: 'relative', aspectRatio: '16 / 9' }}>
<Image
src={cover}
alt="Article cover"
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, 768px"
/>
</div>
The full anti-CLS treatment of fill — sized parents, aspect-ratio, and matching sizes — is in fixing CLS with next/image fill and sizes.
Step 4 — Allowlist remote sources
// next.config.js — remote images 403/throw unless the host is allowlisted.
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example-cdn.com',
// Constrain the path so the optimizer cannot be turned into an
// open image proxy for arbitrary URLs on this host.
pathname: '/catalogue/**',
},
],
},
};
Step 5 — Point at a CDN loader when one already resizes
If a CDN already owns transformations, run only that optimizer, not both. Set a loaderFile so <Image> emits CDN transform URLs instead of /_next/image paths. The full walkthrough is in Next.js Image Custom Loader for a CDN. For enterprise DAM systems that sign URLs with HMAC tokens, see the related next/image with custom loader configurations guide, which covers signature preservation across the generated srcset.
Step 6 — Add a blur placeholder correctly
placeholder="blur" renders a tiny, up-scaled preview until the real image paints, smoothing perceived load. For a static import Next derives the blurDataURL automatically at build time; for a remote src there is no build step to derive it, so you must supply one or the placeholder is ignored.
// Local import: blurDataURL is auto-generated — nothing else to do.
import avatar from '@/assets/avatar.jpg';
<Image src={avatar} alt="Member avatar" width={96} height={96} placeholder="blur" />
// Remote src: you MUST pass blurDataURL yourself, or placeholder does nothing.
// Precompute a ~10px base64 preview at build/upload time (e.g. with sharp or plaiceholder).
<Image
src="https://images.example-cdn.com/catalogue/lamp.jpg"
alt="Desk lamp"
width={600}
height={600}
placeholder="blur"
blurDataURL="data:image/webp;base64,UklGR... (tiny 10px preview)"
/>;
Warning: a large blurDataURL defeats its own purpose — it ships inline in the HTML on every render. Keep the preview around 8–16px on its longest edge and a few hundred bytes; anything larger bloats the document and delays the parser.
Parameter reference
| Prop | Type | Effect |
|---|---|---|
priority |
boolean | Emits fetchpriority="high" and injects <link rel=preload> with matching imagesrcset/imagesizes. One per route. |
sizes |
string | The layout contract; drives which srcset width the browser picks. Required for responsive images. |
fill |
boolean | Absolutely positions the image to fill a sized parent; drops intrinsic width/height. |
quality |
number | Optimizer quality (1–100, default 75). In Next 15+ must be listed in images.qualities. |
placeholder |
'blur' | 'empty' |
'blur' renders blurDataURL (auto for imports) until paint. |
loading |
'lazy' | 'eager' |
lazy (default) for below-fold; never on the LCP image. priority implies eager. |
unoptimized |
boolean | Emits the raw src, skipping /_next/image for this image only. |
When to disable optimization
The optimizer is not always the right layer. unoptimized (per-image or globally via images.unoptimized) makes <Image> emit the raw src while keeping the component’s layout reservation, sizes, and priority behaviour. Reach for it when:
- The image is already an optimized, correctly sized asset (a pre-generated AVIF from your build pipeline) and a second re-encode would only add a lossy pass and latency.
- You use
output: 'export'for a fully static site with no Node server to run/_next/image. Static export cannot run the on-request optimizer, so you either setunoptimized: trueor configure a custom/CDN loader that resizes at the edge — see Next.js Image Custom Loader for a CDN. - The asset is an SVG or a tiny icon where resizing yields nothing.
// This PNG is a pre-optimized sprite; skip the optimizer but keep the sized box.
<Image src="/sprites/logo.png" alt="Logo" width={140} height={32} unoptimized />
Tradeoff: unoptimized forfeits format negotiation and responsive resizing for that image — the browser gets exactly the file you named, at its full byte weight, for every viewport. Use it deliberately, not as a blanket workaround for a sharp install problem.
Tradeoffs and self-hosting
Tradeoff: Vercel vs self-hosted. On Vercel the optimizer runs as a managed, globally cached function. Self-hosting means the /_next/image route runs in your Node server or container, and its cache lives on that instance’s disk — which resets on every deploy and is not shared across replicas. Front it with a CDN keyed on the full optimized URL, and persist or share .next/cache/images.
Warning: sharp in Docker. next start needs sharp installed for optimization; on Alpine base images the prebuilt libvips binary may be missing, causing the optimizer to fall back to the slower @squoosh/lib (removed in newer Next) or to error. Install sharp explicitly and use a glibc base (node:20-slim) or the correct --platform build so the native binary matches the runtime.
Tradeoff: caching /_next/image. The optimizer sets Cache-Control derived from the source’s own headers and minimumCacheTTL. If your upstream sends no-store, the optimizer will not cache aggressively — audit the source headers, covered in Cache-Control headers for image and video assets.
| Failure mode | Cause | Fix |
|---|---|---|
Optimizer throws on remote src |
Host not in remotePatterns |
Add host + constrained pathname |
| First view slow, later views fast | Cold optimizer cache per variant | Warm cache; CDN-front /_next/image |
| Images render huge/blurry | sizes missing → 100vw default |
Set accurate sizes per breakpoint |
Build/runtime error: no sharp |
Native binary missing in container | Install sharp; use glibc base image |
| Safari 14 gets broken image | AVIF forced with no WebP fallback | Keep 'image/webp' after 'image/avif' in formats |
quality prop ignored/errors |
Value not in images.qualities (Next 15+) |
Add the value to the qualities allowlist |
Debugging
Inspect the generated markup and network activity to confirm the optimizer is behaving:
# 1. Confirm the emitted srcset points at /_next/image with the widths you expect.
curl -s https://localhost:3000/ | grep -o '/_next/image[^"]*' | head
# 2. Request one variant with an AVIF-capable Accept header and check the format.
# Expect: content-type: image/avif (falls to image/webp for Safari 14 Accept)
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
'https://localhost:3000/_next/image?url=%2Fhero.jpg&w=1200&q=75' \
| grep -iE 'content-type|cache-control|x-nextjs-cache'
x-nextjs-cache: HIT confirms a warm optimizer cache; MISS on repeat requests means the cache is not persisting (common on serverless without a shared store). In DevTools, the Network panel’s “Img” filter shows the chosen width — if a desktop request is pulling the 3840w file, your sizes is wrong.
Related
- Next.js Image Custom Loader for a CDN — replace
/_next/imagewith a Cloudflare/Imgix/Cloudinary transform loader - Fixing CLS with next/image fill and sizes — eliminate layout shift with sized parents and matching sizes
- Framework & Build-Tool Media Integration — how next/image compares to @nuxt/image, astro:assets, and vite-imagetools
- next/image with custom loader configurations — signed-URL and DAM loaders that preserve HMAC parameters
- Mastering srcset and sizes for responsive layouts — the sizes/srcset model the component depends on
- Cache-Control headers for image and video assets — how source headers shape the optimizer’s cache