---
title: "Image Optimization"
description: "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 cl..."
last_updated: "2026-10-12T06:38:56.845597+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/built-in-features/image-optimization"
---

<details>
<summary>Relevant source files</summary>

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

- [packages/next/src/server/image-optimizer.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts)
- [packages/next/src/server/image-optimizer/transform.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts)
- [packages/next/src/client/legacy/image.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/legacy/image.tsx)
- [packages/next/src/server/og/cache-image-response.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/cache-image-response.ts)
- [packages/next/src/server/next-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts)
- [packages/next/src/build/webpack/loaders/next-image-loader/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/index.ts)
- [packages/next/src/shared/lib/get-img-props.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/get-img-props.ts)
- [packages/next/image-optimizer-transform.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/image-optimizer-transform.js)
- [packages/next/src/client/image-component.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/image-component.tsx)
- [packages/next/src/shared/lib/image-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-loader.ts)
- [packages/next/src/server/image-optimizer/extract-etag.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/extract-etag.ts)
- [packages/next/src/build/webpack/loaders/next-image-loader/blur.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts)
- [packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts)
- [packages/next/image-optimizer-transform.d.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/image-optimizer-transform.d.ts)
- [packages/next/src/shared/lib/image-config.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-config.ts)
- [packages/next/src/server/image-optimizer/image-type.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/image-type.ts)
- [packages/next/src/server/image-optimizer/get-image-size.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/get-image-size.ts)
- [packages/next/image.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/image.js)
- [packages/next/src/server/og/image-response.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/og/image-response.ts)
- [packages/next/legacy/image.js](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/legacy/image.js)
- [packages/next/src/shared/lib/image-blur-svg.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-blur-svg.ts)
- [packages/next/src/api/image.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/api/image.ts)
</details>

## 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L47-L57), [packages/next/src/server/next-server.ts:995-1002](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts#L995-L1002), [packages/next/src/client/image-component.tsx:14-16](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/image-component.tsx#L14-L16), [packages/next/src/build/webpack/loaders/next-image-loader/index.ts:42-62](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/index.ts#L42-L62), [packages/next/src/server/image-optimizer/transform.ts:181-195](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L181-L195)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/get-img-props.ts#L255-L276)

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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/get-img-props.ts#L228-L253)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/image-component.tsx#L283-L291), [packages/next/src/shared/lib/get-img-props.ts:269-276](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/get-img-props.ts#L269-L276)

### 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 Identifier | Description |
| :--- | :--- |
| `default` | Uses the built-in Next.js image optimization endpoint (`/_next/image`). |
| `imgix` | Generates URLs targeting the Imgix cloud image service. |
| `cloudinary` | Generates URLs targeting Cloudinary. |
| `akamai` | Generates URLs targeting Akamai image manager. |
| `custom` | Delegates URL construction entirely to a user-defined custom loader function. |

Sources: [packages/next/src/shared/lib/image-config.ts:1-7](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-config.ts#L1-L7), [packages/next/src/shared/lib/image-loader.ts:5-128](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-loader.ts#L5-L128)

```typescript
  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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-loader.ts#L123-L128)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-loader.ts#L67-L72)

## 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`.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/index.ts#L31-L51)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/get-image-size.ts#L3-L8), [packages/next/src/build/webpack/loaders/next-image-loader/index.ts:43-51](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/index.ts#L43-L51)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/index.ts#L43-L51)

### 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 Name | Value | Purpose |
| :--- | :--- | :--- |
| `BLUR_IMG_SIZE` | `8` | The target pixel size for the largest dimension of the resized blur buffer. |
| `BLUR_QUALITY` | `70` | The 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts#L4-L6)

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts#L40-L83)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-image-loader/blur.ts#L56-L65)

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

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-blur-svg.ts#L4-L33)

## 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L178-L185)

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

| Config Property | Default Value | Purpose |
| :--- | :--- | :--- |
| `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/image` | The base URL path prefix for the image optimization endpoint. |
| `loader` | `'default'` | The built-in image loader identifier (`VALID_LOADERS`). |
| `minimumCacheTTL` | `14400` | The minimum cache time-to-live in seconds (4 hours). |
| `formats` | `['image/webp']` | Accepted output image formats for negotiation. |
| `maximumRedirects` | `3` | Maximum number of allowed upstream redirects when fetching remote images. |
| `maximumResponseBody` | `50_000_000` | Maximum permitted upstream response size in bytes (50 MB). |
| `dangerouslyAllowLocalIP` | `false` | Permits fetching from private or local IP addresses when enabled. |
| `dangerouslyAllowSVG` | `false` | Permits optimization and serving of SVG images. |
| `contentSecurityPolicy` | `script-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. |
| `unoptimized` | `false` | Disables image optimization globally when set to true. |
| `customCacheHandler` | `false` | Routes image caching to a custom `cacheHandler` implementation. |

Sources: [packages/next/src/shared/lib/image-config.ts:237-261](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-config.ts#L237-L261)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-config.ts#L115-L116), [packages/next/src/server/image-optimizer.ts:29](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L29)

### 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L14-L15)

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-config.ts#L36-L67)

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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/image-config.ts#L188-L235)

## 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 Type | MIME Constant |
| :--- | :--- |
| SVG | `image/svg+xml` |
| ICO | `image/x-icon` |
| ICNS | `image/x-icns` |
| BMP | `image/bmp` |
| JXL | `image/jxl` |
| HEIC | `image/heic` |

Sources: [packages/next/src/server/image-optimizer/transform.ts:213-260](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L213-L260), [packages/next/src/server/image-optimizer/transform.ts:31](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L31)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L30), [packages/next/src/server/image-optimizer/transform.ts:240-251](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L240-L251)

### 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L93-L135)

```typescript
  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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L181-L194)

### 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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L1-L5), [packages/next/src/server/image-optimizer/transform.ts:201-272](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/transform.ts#L201-L272)

```javascript
module.exports = require('./dist/server/image-optimizer/transform')
```
Sources: [packages/next/image-optimizer-transform.js:1-1](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/image-optimizer-transform.js#L1-L1)

## 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.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L69-L90)

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L108-L158)

> [!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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L118-L122)

### 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`.

```typescript
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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/extract-etag.ts#L1-L28)

| Function Name | Input Parameters | Output Format |
| :--- | :--- | :--- |
| `getHash` | `items: (string \| number \| Buffer)[]` | `base64url` string |
| `extractEtag` | `etag: string \| null \| undefined`, `imageBuffer: Buffer` | `base64url` string |
| `getImageEtag` | `image: Buffer` | `base64url` string |

Sources: [packages/next/src/server/image-optimizer/extract-etag.ts:3-28](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer/extract-etag.ts#L3-L28)

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](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/image-optimizer.ts#L26)

## Related

- [Server Architecture](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/server-architecture)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
