Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Image Optimization

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Next.js Image Optimization provides an integrated pipeline for resizing, reformatting, and caching raster and vector graphics to maximize web performance and core web vitals. The subsystem spans client-side attribute generation via [[Metadata Generation]] and component props, build-time dimension inspection and placeholder creation, server-side request routing and security enforcement across [[Server Architecture]], and robust on-demand image transformation backed by Sharp with content negotiation for modern formats like AVIF and WebP.

Sources: packages/next/src/server/image-optimizer.ts:47-57, packages/next/src/server/next-server.ts:995-1002, packages/next/src/client/image-component.tsx:14-16, packages/next/src/build/webpack/loaders/next-image-loader/index.ts:42-62, packages/next/src/server/image-optimizer/transform.ts:181-195

Client Attribute Generation and Loaders

Public API and Attribute Generation

The public client image component exported from [[Metadata Generation]] (packages/next/src/api/image.ts) builds upon getImgProps to transform user-supplied properties into attributes for the underlying <img> element. During attribute generation, generateImgAttrs inspects the image configuration and dimensions to compute responsive srcSet strings and default sizes fallback values.

const { widths, kind } = getWidths(config, width, sizes)
const last = widths.length - 1
 
return {
  sizes: !sizes && kind === 'w' ? '100vw' : sizes,
  srcSet: widths
    .map(
      (w, i) =>
        `${loader({ config, src, quality, width: w })} ${
          kind === 'w' ? w : i + 1
        }${kind}`
    )
    .join(', '),
 
  // It's intended to keep `src` the last attribute because React updates
  // attributes in order. If we keep `src` the first one, Safari will
  // immediately start to fetch `src`, before `sizes` and `srcSet` are even
  // updated by React. That causes multiple unnecessary requests if `srcSet`
  // and `sizes` are defined.
  // This bug cannot be reproduced in Chrome or Firefox.
  src: loader({ config, src, quality, width: widths[last] }),
}

Sources: packages/next/src/shared/lib/get-img-props.ts:255-276

When the unoptimized flag is enabled, or when asset URLs utilize protocol-relative or data schemes, optimization is bypassed. For local static assets rendered without optimization when a deployment identifier is present, the framework appends the ?dpl= query parameter to ensure proper cache busting, while skipping assets flagged as immutable static resources unless an asset token is available.

if (unoptimized) {
  if (src.startsWith('/') && !src.startsWith('//')) {
    let deploymentId = getDeploymentId()
    if (src.includes('/_next/static/immutable') && !getAssetToken()) {
      // immutable static asset and supported by platform, don't add `?dpl=`
      deploymentId = undefined
    } else if (deploymentId) {
      // We unfortunately can't easily use `new URL()` here, because it normalizes the URL which causes
      // double-encoding with the `encodeURIComponent(src)` below
      const qIndex = src.indexOf('?')
      if (qIndex !== -1) {
        const params = new URLSearchParams(src.slice(qIndex + 1))
        const srcDpl = params.get('dpl')
        if (!srcDpl) {
          // src is missing the dpl parameter, but we have a deploymentId, so add it to the src URL
          params.append('dpl', deploymentId)
          src = src.slice(0, qIndex) + '?' + params.toString()
        }
      } else {
        // src is missing the dpl parameter, but we have a deploymentId, so add it to the src URL
        src = src + `?dpl=${deploymentId}`
      }
    }
  }
  return { src, srcSet: undefined, sizes: undefined }
}

Sources: packages/next/src/shared/lib/get-img-props.ts:228-253

Warning

React updates element attributes in the exact order they are spread or declared. To prevent Safari from prematurely fetching the primary src before responsive sizes and srcSet descriptors are populated by React, the src attribute is intentionally placed as the final property on the rendered <img> element.

Sources: packages/next/src/client/image-component.tsx:283-291, packages/next/src/shared/lib/get-img-props.ts:269-276

Loader URL Generation and Configuration

Image loaders translate source paths, requested widths, and qualities into concrete URLs. The built-in default loader validates presence parameters, manages deployment ID query tokens, and formats the optimization request path.

Valid Loader IdentifierDescription
defaultUses the built-in Next.js image optimization endpoint (/_next/image).
imgixGenerates URLs targeting the Imgix cloud image service.
cloudinaryGenerates URLs targeting Cloudinary.
akamaiGenerates URLs targeting Akamai image manager.
customDelegates URL construction entirely to a user-defined custom loader function.

Sources: packages/next/src/shared/lib/image-config.ts:1-7, packages/next/src/shared/lib/image-loader.ts:5-128

  const q = findClosestQuality(quality, config)
 
  return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${q}${
    src.startsWith('/') && deploymentId ? `&dpl=${deploymentId}` : ''
  }`
}

Sources: packages/next/src/shared/lib/image-loader.ts:123-128

Caution

Protocol-relative image sources starting with // are strictly rejected by the default loader in development mode and must be explicitly converted to absolute URLs utilizing either http:// or https://.

Sources: packages/next/src/shared/lib/image-loader.ts:67-72

Build Time Dimensions and Placeholders

Static Image Imports and Dimension Inspection

When static assets are imported in source code, the next-image-loader processes the binary buffer via getImageSize to extract intrinsic width and height dimensions. Filename hashes are interpolated into /static/media/[name].[hash:8].[ext], with jpg extensions normalized to jpeg.

const interpolatedName = loaderUtils.interpolateName(
  this,
  '/static/media/[name].[hash:8].[ext]',
  opts
)
const outputPath = assetPrefix + '/_next' + interpolatedName
let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
  extension = 'jpeg'
}
 
const imageSizeSpan = imageLoaderSpan.traceChild('image-size-calculation')
const imageSize = await imageSizeSpan.traceAsyncFn(() =>
  getImageSize(content).catch((err) => err)
)
 
if (imageSize instanceof Error) {
  const err = imageSize
  err.name = 'InvalidImageFormatError'
  throw err
}

Sources: packages/next/src/build/webpack/loaders/next-image-loader/index.ts:31-51

If the underlying buffer cannot be decoded, getImageSize captures the parsing failure and assigns the InvalidImageFormatError name to the thrown error instance.

export async function getImageSize(buffer: Buffer): Promise<{
  width?: number
  height?: number
}> {
  const { width, height } = imageSizeOf(buffer)
  return { width, height }

Sources: packages/next/src/server/image-optimizer/get-image-size.ts:3-8, packages/next/src/build/webpack/loaders/next-image-loader/index.ts:43-51

Warning

If getImageSize encounters an unparseable or corrupted payload, it throws an error that the loader explicitly catches and renames to InvalidImageFormatError prior to failing the webpack compilation step.

Sources: packages/next/src/build/webpack/loaders/next-image-loader/index.ts:43-51

Blur Placeholder Generation and SVG Formatting

The blur generation pipeline evaluates whether asset extensions and animation frames permit placeholder creation using fixed dimension and quality constraints.

Constant NameValuePurpose
BLUR_IMG_SIZE8The target pixel size for the largest dimension of the resized blur buffer.
BLUR_QUALITY70The compression quality setting passed to the image optimizer.
VALID_BLUR_EXT['jpeg', 'png', 'webp', 'avif']The array of allowed image formats eligible for blur generation.

Sources: packages/next/src/build/webpack/loaders/next-image-loader/blur.ts:4-6

if (VALID_BLUR_EXT.includes(extension) && !isAnimated(content)) {
  // Shrink the image's largest dimension
  if (imageSize.width >= imageSize.height) {
    blurWidth = BLUR_IMG_SIZE
    blurHeight = Math.max(
      Math.round((imageSize.height / imageSize.width) * BLUR_IMG_SIZE),
      1
    )
  } else {
    blurWidth = Math.max(
      Math.round((imageSize.width / imageSize.height) * BLUR_IMG_SIZE),
      1
    )
    blurHeight = BLUR_IMG_SIZE
  }
 
  if (isDev) {
    // During `next dev`, we don't want to generate blur placeholders with webpack
    // because it can delay starting the dev server. Instead, we inline a
    // special url to lazily generate the blur placeholder at request time.
    const prefix = 'http://localhost'
    const url = new URL(`${basePath || ''}/_next/image`, prefix)
    url.searchParams.set('url', outputPath)
    url.searchParams.set('w', String(blurWidth))
    url.searchParams.set('q', String(BLUR_QUALITY))
    blurDataURL = url.href.slice(prefix.length)
  } else {
    const resizeImageSpan = tracing('image-resize')
    const resizedImage = await resizeImageSpan.traceAsyncFn(() =>
      optimizeImage({
        buffer: content,
        width: blurWidth,
        height: blurHeight,
        contentType: `image/${extension}`,
        quality: BLUR_QUALITY,
      })
    )
    const blurDataURLSpan = tracing('image-base64-tostring')
    blurDataURL = blurDataURLSpan.traceFn(
      () =>
        `data:image/${extension};base64,${resizedImage.toString('base64')}`
    )
  }
}

Sources: packages/next/src/build/webpack/loaders/next-image-loader/blur.ts:40-83

Note

During development mode (isDev equals true), getBlurImage bypasses heavy webpack-side image compression to prevent startup delays, instead generating a lazy /_next/image URL endpoint query.

Sources: packages/next/src/build/webpack/loaders/next-image-loader/blur.ts:56-65

Shared SVG placeholder utilities scale the resulting base64 data URL using an SVG filter definition containing Gaussian blur (stdDeviation='20') and color matrices.

export function getImageBlurSvg({
  widthInt,
  heightInt,
  blurWidth,
  blurHeight,
  blurDataURL,
  objectFit,
}: {
  widthInt?: number
  heightInt?: number
  blurWidth?: number
  blurHeight?: number
  blurDataURL: string
  objectFit?: string
}): string {
  const std = 20
  const svgWidth = blurWidth ? blurWidth * 40 : widthInt
  const svgHeight = blurHeight ? blurHeight * 40 : heightInt
 
  const viewBox =
    svgWidth && svgHeight ? `viewBox='0 0 ${svgWidth} ${svgHeight}'` : ''
  const preserveAspectRatio = viewBox
    ? 'none'
    : objectFit === 'contain'
      ? 'xMidYMid'
      : objectFit === 'cover'
        ? 'xMidYMid slice'
        : 'none'
 
  return `%3Csvg xmlns='http://www.w3.org/2000/svg' ${viewBox}%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='${std}'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='${std}'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='${preserveAspectRatio}' style='filter: url(%23b);' href='${blurDataURL}'/%3E%3C/svg%3E`

Sources: packages/next/src/shared/lib/image-blur-svg.ts:4-33

Request Dispatch and Security Checks

Parameter Validation and Configuration Handling

Incoming requests targeting the image optimization server route are intercepted by the ImageOptimizerCache.validateParams method. This method extracts query string parameters including url, w (width), and q (quality), validating them against the application's runtime configuration (NextConfigRuntime).

Sources: packages/next/src/server/image-optimizer.ts:178-185

Default configuration properties defined in imageConfigDefault provide fallback constraints for sizes, domains, minimum cache time-to-live, and remote patterns.

Config PropertyDefault ValuePurpose
deviceSizes[640, 750, 828, 1080, 1200, 1920, 2048, 3840]Width breakpoints used for responsive layouts across device viewports.
imageSizes[32, 48, 64, 96, 128, 256, 384]Smaller width sizes used for icons and inline images.
path/_next/imageThe base URL path prefix for the image optimization endpoint.
loader'default'The built-in image loader identifier (VALID_LOADERS).
minimumCacheTTL14400The minimum cache time-to-live in seconds (4 hours).
formats['image/webp']Accepted output image formats for negotiation.
maximumRedirects3Maximum number of allowed upstream redirects when fetching remote images.
maximumResponseBody50_000_000Maximum permitted upstream response size in bytes (50 MB).
dangerouslyAllowLocalIPfalsePermits fetching from private or local IP addresses when enabled.
dangerouslyAllowSVGfalsePermits optimization and serving of SVG images.
contentSecurityPolicyscript-src 'none'; frame-src 'none'; sandbox;CSP header injected when serving SVG files.
contentDispositionType'attachment'Controls the Content-Disposition header disposition value.
remotePatterns[]Array of allowed remote source URL patterns.
unoptimizedfalseDisables image optimization globally when set to true.
customCacheHandlerfalseRoutes image caching to a custom cacheHandler implementation.

Sources: packages/next/src/shared/lib/image-config.ts:237-261

Caution

Setting dangerouslyAllowLocalIP to true bypasses internal Server-Side Request Forgery (SSRF) checks against private IP ranges, allowing the image optimizer to fetch resources from local network addresses.

Sources: packages/next/src/shared/lib/image-config.ts:115-116, packages/next/src/server/image-optimizer.ts:29

SSRF Prevention and Remote Pattern Matching

To prevent Server-Side Request Forgery vulnerabilities, remote image URLs are evaluated against configured remotePatterns and legacy domains lists via hasRemoteMatch. Local paths are verified using hasLocalMatch.

Sources: packages/next/src/server/image-optimizer.ts:14-15

export type RemotePattern = {
  /**
   * Must be `http` or `https`.
   */
  protocol?: 'http' | 'https'
 
  /**
   * Can be literal or wildcard.
   * Single `*` matches a single subdomain.
   * Double `**` matches any number of subdomains.
   */
  hostname: string
 
  /**
   * Can be literal port such as `8080` or empty string
   * meaning no port.
   */
  port?: string
 
  /**
   * Can be literal or wildcard.
   * Single `*` matches a single path segment.
   * Double `**` matches any number of path segments.
   */
  pathname?: string
 
  /**
   * Can be literal query string such as `?v=1` or
   * empty string meaning no query string.
   */
  search?: string
}

Sources: packages/next/src/shared/lib/image-config.ts:36-67

The prepareImageConfig helper ensures options are sorted and structured consistently across server runtime contexts and environment configurations, handling client-side and server-side pattern merging.

export function prepareImageConfig(
  envConfig?: ImageConfigForRendering,
  contextConfig?: ImageConfigComplete
): PreparedImageConfig {
  const envKey = envConfig ?? missingImageConfig
  const contextKey = contextConfig ?? missingImageConfig
  let preparedByContext = preparedImageConfigs.get(envKey)
  if (preparedByContext) {
    const cached = preparedByContext.get(contextKey)
    if (cached) {
      return cached
    }
  }
 
  const source: ImageConfigForRendering =
    envConfig || contextConfig || imageConfigDefault
  const deviceSizes = [...source.deviceSizes].sort((a, b) => a - b)
  const imageSizes = [...source.imageSizes]
  const prepared: PreparedImageConfig = {
    deviceSizes,
    imageSizes,
    allSizes: [...deviceSizes, ...imageSizes].sort((a, b) => a - b),
    qualities:
      source.qualities === undefined
        ? undefined
        : [...source.qualities].sort((a, b) => a - b),
    path: source.path,
    loader: source.loader,
    dangerouslyAllowSVG: source.dangerouslyAllowSVG,
    unoptimized: source.unoptimized,
    domains: source.domains,
    remotePatterns: source.remotePatterns,
    // The inlined browser options supply their own patterns. During SSR the
    // context supplies security-sensitive patterns omitted from the inline data.
    localPatterns:
      typeof window === 'undefined' && contextConfig !== undefined
        ? contextConfig.localPatterns
        : source.localPatterns,
    output: source.output,
  }
 
  if (!preparedByContext) {
    preparedByContext = new WeakMap()
    preparedImageConfigs.set(envKey, preparedByContext)
  }
  preparedByContext.set(contextKey, prepared)
  return prepared
}

Sources: packages/next/src/shared/lib/image-config.ts:188-235

Image Transformation and Format Negotiation

Content-Type Detection and Bypass Handling

Incoming upstream image buffers are evaluated for content type using detectContentType. If the content type is missing, non-image-prefixed, or contains commas, validation throws an ImageError with status code 400. SVG files trigger an error unless dangerouslyAllowSVG is enabled in configuration. Animated images detected via isAnimated and formats designated as bypass types are returned immediately as raw upstream buffers without resizing or re-encoding.

Bypass TypeMIME Constant
SVGimage/svg+xml
ICOimage/x-icon
ICNSimage/x-icns
BMPimage/bmp
JXLimage/jxl
HEICimage/heic

Sources: packages/next/src/server/image-optimizer/transform.ts:213-260, packages/next/src/server/image-optimizer/transform.ts:31

Warning

Animated variants of image/webp, image/png, and image/gif match ANIMATABLE_TYPES and bypass optimization entirely, returning the original buffer to preserve frame animation data.

Sources: packages/next/src/server/image-optimizer/transform.ts:30, packages/next/src/server/image-optimizer/transform.ts:240-251

Sharp Initialization and Resizing Pipeline

The getSharp function lazily loads the sharp module, blocking all default foreign load operations except for VipsForeignLoadHeif, VipsForeignLoadJpeg, VipsForeignLoadNsgif, VipsForeignLoadPng, VipsForeignLoadSvg, VipsForeignLoadTiff, and VipsForeignLoadWebp. Concurrency is automatically scaled down based on whether execution occurs in development or production.

Sources: packages/next/src/server/image-optimizer/transform.ts:93-135

  transformer.avif({
    // Scale the quality to try and match webp. This ratio was derived
    // from sharp's default 80 (webp) and 50 (avif), and then verified
    // using dssim and ssimulacra2 visual quality tests.
    quality: Math.max(Math.round(quality * (50 / 80)), 1),
    effort: 3,
  })
} else if (contentType === WEBP) {
  transformer.webp({ quality })
} else if (contentType === PNG) {
  transformer.png({ quality })
} else if (contentType === JPEG) {
  transformer.jpeg({ quality, mozjpeg })
}

Sources: packages/next/src/server/image-optimizer/transform.ts:181-194

Format Negotiation and API Surface

Format negotiation inspects the requested mimeType. If absent, upstream WEBP or AVIF formats are downleveled to JPEG when client request headers do not advertise support. The public transform API contract is exported through next/image-optimizer-transform to run lightweight image transformations within a child process.

Sources: packages/next/src/server/image-optimizer/transform.ts:1-5, packages/next/src/server/image-optimizer/transform.ts:201-272

module.exports = require('./dist/server/image-optimizer/transform')

Sources: packages/next/image-optimizer-transform.js:1-1

Cache Storage and ETag Revalidation

Disk LRU and Incremental Cache Integration

Cached optimized images are stored on disk using the ImageOptimizerCache class, which coordinates with a disk LRU cache initialized via getOrInitDiskLRU. When cache directories are scanned, initCacheEntries reads existing keys, file sizes, and expiration timestamps from disk, sorting them chronologically to replay them into the LRU store. Individual cache entries are written to and read from disk directories named after the cache key.

async function initCacheEntries(
  cacheDir: string
): Promise<Array<{ key: string; size: number; expireAt: number }>> {
  const cacheKeys = await promises.readdir(cacheDir).catch(() => [])
  const entries: Array<{ key: string; size: number; expireAt: number }> = []
 
  for (const cacheKey of cacheKeys) {
    try {
      const { expireAt, buffer } = await readFromCacheDir(cacheDir, cacheKey)
      entries.push({
        key: cacheKey,
        size: buffer.byteLength,
        expireAt,
      })
    } catch {
      // Skip entries that can't be read from disk
    }
  }
 
  // Sort oldest-first so we can replay them chronologically into LRU
  return entries.sort((a, b) => a.expireAt - b.expireAt)
}

Sources: packages/next/src/server/image-optimizer.ts:69-90

async function writeToCacheDir(
  cacheDir: string,
  cacheKey: string,
  extension: string,
  maxAge: number,
  expireAt: number,
  buffer: Buffer,
  etag: string,
  upstreamEtag: string
) {
  if (buffer.byteLength === 0) {
    throw new Error(
      'Invariant: cannot write an empty buffer to the image cache'
    )
  }
 
  const dir = join(/* turbopackIgnore: true */ cacheDir, cacheKey)
  const filename = join(
    /* turbopackIgnore: true */
    dir,
    `${maxAge}.${expireAt}.${etag}.${upstreamEtag}.${extension}`
  )
 
  await promises.rm(dir, { recursive: true, force: true }).catch(() => {})
 
  await promises.mkdir(dir, { recursive: true })
  await promises.writeFile(filename, buffer)
}
 
async function readFromCacheDir(cacheDir: string, cacheKey: string) {
  const dir = join(/* turbopackIgnore: true */ cacheDir, cacheKey)
  const files = await promises.readdir(dir)
  const file = files[0]
  if (!file) {
    throw new Error(
      `Invariant: cache entry "${cacheKey}" not found in dir "${cacheDir}"`
    )
  }
  const [maxAgeSt, expireAtSt, etag, upstreamEtag, extension] = file.split(
    '.',
    5
  )
  const filePath = join(/* turbopackIgnore: true */ dir, file)
  const buffer = await promises.readFile(/* turbopackIgnore: true */ filePath)
  if (buffer.byteLength === 0) {
    throw new Error(`Invariant: image cache entry "${cacheKey}" is empty`)
  }
  const expireAt = Number(expireAtSt)
  const maxAge = Number(maxAgeSt)
  return { maxAge, expireAt, etag, upstreamEtag, buffer, extension }
}

Sources: packages/next/src/server/image-optimizer.ts:108-158

Warning

Writing an empty buffer (buffer.byteLength === 0) to the image cache throws an immediate invariant error and prevents disk write operations.

Sources: packages/next/src/server/image-optimizer.ts:118-122

ETag Generation and Conditional Request Handling

ETags for optimized images are generated using SHA-256 hashing via getHash, which serializes strings, numbers, and buffers into a base64url-encoded digest. The extractEtag function checks for an upstream ETag; if present, it base64url-encodes the string. If absent, it computes a fresh ETag directly from the image buffer using getImageEtag.

import { createHash } from 'crypto'
 
export function getHash(items: (string | number | Buffer)[]) {
  const hash = createHash('sha256')
  for (let item of items) {
    if (typeof item === 'number') hash.update(String(item))
    else {
      hash.update(item)
    }
  }
  // See https://en.wikipedia.org/wiki/Base64#URL_applications
  return hash.digest('base64url')
}
 
export function extractEtag(
  etag: string | null | undefined,
  imageBuffer: Buffer
) {
  if (etag) {
    // upstream etag needs to be base64url encoded due to weak etag signature
    // as we store this in the cache-entry file name.
    return Buffer.from(etag).toString('base64url')
  }
  return getImageEtag(imageBuffer)
}
 
export function getImageEtag(image: Buffer) {
  return getHash([image])

Sources: packages/next/src/server/image-optimizer/extract-etag.ts:1-28

Function NameInput ParametersOutput Format
getHashitems: (string | number | Buffer)[]base64url string
extractEtagetag: string | null | undefined, imageBuffer: Bufferbase64url string
getImageEtagimage: Bufferbase64url string

Sources: packages/next/src/server/image-optimizer/extract-etag.ts:3-28

Conditional requests are finalized using sendEtagResponse, which verifies cache headers and responds with appropriate status codes when client validation matches stored asset identifiers.

Sources: packages/next/src/server/image-optimizer.ts:26

Was this page helpful?