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.

next/image optimization request flow The Image component emits a srcset of /_next/image URLs. The browser requests one width; the optimizer checks its cache; on a miss it invokes sharp to resize and re-encode per the Accept header and images.formats config, then caches and returns the result behind a CDN. <Image> emits srcset of /_next/image URLs GET ?w&q /_next/image reads Accept cache lookup by url+w+q+fmt HIT → serve MISS sharp resize + re-encode AVIF/WebP CDN / edge caches by URL immutable TTL Browser picks width by sizes + DPR

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.

  • deviceSizes is used when the image is viewport-relative — that is, when your sizes contains a viewport unit like 100vw or 50vw. Next multiplies each device size against the layout to build the srcset, so these values should map to the common device widths and DPRs in your audience.
  • imageSizes is used when the image is a fixed size (a sizes of 240px, 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. imageSizes values are only ever used with deviceSizes, 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 set unoptimized: true or 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.