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
-
Let Next.js optimise. The component requests a
/_next/imageURL 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’sAcceptheader. You store one original per image. This is the quickest to adopt, and it costs CPU on the server and a cache. -
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 customloaderthat builds the CDN URL for a requested width, so Next.js only produces thesrcset. 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"
/>
);
}
-
widthandheightare the intrinsic size and reserve space. They are required for remote images unless you usefill. -
sizesis the single most effective prop. Without it, Next.js generates asrcsetfor fixed densities, and with it a width-basedsrcsetthe browser can choose from. -
fillmakes the image fill its positioned parent, for when the size is unknown. Give the parentposition: relativeand a size, for example withaspect-ratio, and setobject-fit. Always combinefillwithsizes.
<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;
-
formatslists 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. -
remotePatternsis an allow list. Without it, remote images are rejected, which stops the optimiser from becoming an open proxy. Be as specific as possible. -
minimumCacheTTLsets 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.