Next.js Image with BlurHash and Custom Loaders

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. This page adds the media-pipeline view to Images, Fonts and Scripts, which is the reference for the component, its configuration defaults, preload and the Pages Router. It follows the Next.js 16 line (npm next 16.4.0 on the baseline date). Verify prop names against the Image component reference for the exact version in your project.

Two ways to use next/image with a media backend

  1. Let Next.js optimise. The component requests a /_next/image URL with the source, width and quality as query parameters, and the Next.js server (or the platform’s image service) fetches the original, resizes it and returns AVIF or WebP according to the browser’s Accept header. You store one original per image. This is the quickest to adopt, and it costs CPU on the server and a cache.

  2. Use the variants from your own pipeline. The backend already produced w320.avif, w640.avif, and so on, see responsive images and thumbnails. Use a custom loader that builds the CDN URL for a requested width, so Next.js only produces the srcset. The browser then downloads the pre-optimised file straight from the CDN.

Basic usage

import Image from 'next/image';

export function Cover({ cover }: { cover: { url: string; width: number; height: number; alt: string } }) {
  return (
    <Image
      src={cover.url}
      alt={cover.alt}
      width={cover.width}
      height={cover.height}
      sizes="(min-width: 60em) 20vw, 50vw"
    />
  );
}
  • width and height are the intrinsic size and reserve space. They are required for remote images unless you use fill.

  • sizes is the single most effective prop. Without it, Next.js generates a srcset for fixed densities, and with it a width-based srcset the browser can choose from.

  • fill makes the image fill its positioned parent, for when the size is unknown. Give the parent position: relative and a size, for example with aspect-ratio, and set object-fit. Always combine fill with sizes.

<div style={{ position: 'relative', aspectRatio: '2 / 3' }}>
  <Image src={cover.url} alt={cover.alt} fill sizes="(min-width: 60em) 20vw, 50vw" style={{ objectFit: 'cover' }} />
</div>

Formats, remote patterns and caching

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com', pathname: '/covers/**' }],
    minimumCacheTTL: 60 * 60 * 24 * 30,
  },
};
export default nextConfig;
  • formats lists the output formats in order of preference. The default is WebP only. AVIF compresses smaller and encodes more slowly, and each format is cached separately.

  • remotePatterns is an allow list. Without it, remote images are rejected, which stops the optimiser from becoming an open proxy. Be as specific as possible.

  • minimumCacheTTL sets the minimum lifetime of an optimised image. Next.js has no invalidation, so use content-hashed URLs from the backend rather than overwriting an image at the same URL.

References: formats and remotePatterns.

placeholder="blur" and blurDataURL

placeholder="blur" shows a blurred image until the real one loads. Static imports (import cover from './cover.jpg') get a generated blurDataURL. For remote images you must supply it, and that is where the BlurHash from the API fits: decode it on the server, once, into a small data URL.

npm install blurhash sharp
// lib/blurhash.ts (server only)
import { decode } from 'blurhash';
import sharp from 'sharp';

export async function blurhashToDataUrl(hash: string, width = 16, height = 16): Promise<string> {
  const pixels = decode(hash, width, height);
  const png = await sharp(Buffer.from(pixels), { raw: { width, height, channels: 4 } }).png().toBuffer();
  return `data:image/png;base64,${png.toString('base64')}`;
}
// app/books/[id]/page.tsx  (a Server Component)
import Image from 'next/image';
import { blurhashToDataUrl } from '@/lib/blurhash';

export default async function BookPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const book = await getBook(id);                       // { cover: { url, width, height, blurhash } }
  const blurDataURL = await blurhashToDataUrl(book.cover.blurhash);
  return (
    <Image
      src={book.cover.url}
      alt={book.title}
      width={book.cover.width}
      height={book.cover.height}
      sizes="(min-width: 60em) 20vw, 50vw"
      placeholder="blur"
      blurDataURL={blurDataURL}
    />
  );
}

The decoded size of 16 by 16 keeps the data URL small, since the same bytes are in every HTML page that shows the image. Next.js blurs the data URL, and you can cache the result with the backend response. Use the 'use cache' directive or a memoised function if the decode shows in a profile, see Next.js caching. Client-side alternative: a canvas, see BlurHash in the browser.

A custom loader for your CDN

A loader is a function that receives src, width and quality and returns the URL for that width. With pre-generated variants:

// lib/media-loader.ts
import type { ImageLoaderProps } from 'next/image';

const WIDTHS = [320, 640, 1280];

export default function mediaLoader({ src, width }: ImageLoaderProps): string {
  // src is the image id, for example "b81f"; choose the smallest variant that is at least `width`
  const chosen = WIDTHS.find((w) => w >= width) ?? WIDTHS[WIDTHS.length - 1];
  return `https://cdn.example.com/covers/${src}/w${chosen}.avif`;
}
// components/cover.tsx -- a loader is a function prop, so it needs a Client Component
'use client';

import Image from 'next/image';
import mediaLoader from '@/lib/media-loader';

export function Cover({ id }: { id: string }) {
  return <Image loader={mediaLoader} src={id} alt="Cover" width={640} height={960} sizes="50vw" />;
}

For an image CDN with URL transformations, the loader builds that URL. For Cloudinary, imgix and similar services:

export const cloudinaryLoader = ({ src, width, quality }: ImageLoaderProps) =>
  `https://res.cloudinary.com/<cloud>/image/upload/f_auto,q_${quality ?? 'auto'},w_${width}/${src}`;

export const imgixLoader = ({ src, width, quality }: ImageLoaderProps) =>
  `https://example.imgix.net/${src}?auto=format&w=${width}&q=${quality ?? 75}`;

Set a loader per component, which only works in a Client Component because functions cannot be passed from a Server Component, or globally with the images.loader option set to custom and images.loaderFile. The global setting also works in Server Components and is the better default. With a custom loader, Next.js does not optimise the image itself, so the formats setting does not apply: the CDN chooses the format. References: loader, Cloudinary documentation and imgix rendering API. The managed image-processing options of each cloud are on the cloud pages: AWS, Google Cloud and Azure.

Next.js 16: preload instead of priority

The LCP image used to take the priority prop. In Next.js 16 priority is deprecated. Use preload to insert a <link rel="preload"> for the image in the head, or loading="eager" or fetchPriority="high" when preloading is not what you want (for example when several images could be the LCP element depending on the viewport).

<Image src={hero.url} alt={hero.alt} width={1920} height={1080} sizes="100vw" preload />
<Image src={hero.url} alt={hero.alt} width={1920} height={1080} sizes="100vw" fetchPriority="high" />

Do not combine preload with loading or fetchPriority. The full rules, with the reasons, are in Images, Fonts and Scripts.

Route handlers and server actions

Uploads and variant URLs can also go through Next.js route handlers and server actions, which act as a backend for frontend, see route handlers and BFF. Keep heavy processing out of them (see upload pipelines), and use them to hand out pre-signed URLs and to read status.