Next.js Image Loaders: Custom URLs, Global Configuration, and Provider Integration
Learn how Next.js Image loaders generate optimization URLs, configure them per image or globally, restrict remote sources, and avoid common failures.
Direct answer: A Next.js Image loader is a function that receives an image source, a requested width, and an optional quality value, then returns the URL that should serve the transformed image. The loader does not resize or compress the file itself; it builds a URL for an image optimization service that performs or serves the transformation.
The next/image component uses Next.js Image Optimization by default. Use a custom loader when your images are handled by an external CDN or transformation service. You can attach a loader to one <Image> instance with the loader prop, or configure one project-wide with images.loader: 'custom' and images.loaderFile. The URL format must match the provider you choose.
How a Next.js loader works
When you render an image, Next.js asks the loader for a URL at an appropriate width. The documented loader arguments are:
| Argument | Meaning | What your loader should do |
|---|---|---|
src |
Source path or URL | Encode it according to the provider’s URL syntax |
width |
Requested output width in pixels | Pass it as the provider’s width or resize parameter |
quality |
Requested quality, when supplied | Map it to the provider’s quality parameter or choose a documented fallback |
Next.js may call the loader more than once for responsive sizes. Your function should be deterministic: the same inputs should produce the same URL.
Per-image custom loader
A per-image loader is useful when only a few images use an external service or when different providers need different URL schemes.
import Image, { type ImageLoaderProps } from 'next/image';
const cloudImageLoader = ({ src, width, quality }: ImageLoaderProps) => {
const q = quality ?? 75;
return `https://img.example.com/resize?url=${encodeURIComponent(src)}&w=${width}&q=${q}`;
};
export default function ProductImage() {
return (
<Image
loader={cloudImageLoader}
src="https://images.example.com/products/shoe.jpg"
alt="Running shoe"
width={1200}
height={900}
quality={80}
/>
);
}
This URL is only an example. Replace the host and query names with the exact syntax documented by your provider. Some services put the source path in the path, sign every request, use encoded transformation segments, or require an account identifier.
Using a loader with a local source
import Image from 'next/image';
const loader = ({ src, width, quality }) =>
`https://cdn.example.com${src}?w=${width}&q=${quality ?? 75}`;
export function Logo() {
return (
<Image
loader={loader}
src="/images/logo.png"
alt="Company logo"
width={320}
height={80}
/>
);
}
Make sure the provider can access the path represented by src. A relative path such as /images/logo.png is not automatically uploaded to the external service.
Configure one loader for the whole project
For a shared provider, put the loader in a project-root-relative file and reference it from next.config.js.
// lib/image-loader.js
export default function imageLoader({ src, width, quality }) {
const q = quality ?? 75;
return `https://img.example.com/resize?url=${encodeURIComponent(src)}&w=${width}&q=${q}`;
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.js',
},
};
module.exports = nextConfig;
The loader file must export a default function that returns a URL string. The per-instance loader prop remains available for exceptions.
TypeScript project-wide loader
// lib/image-loader.ts
import type { ImageLoaderProps } from 'next/image';
export default function imageLoader({ src, width, quality }: ImageLoaderProps) {
const q = quality ?? 75;
return `https://img.example.com/resize?url=${encodeURIComponent(src)}&w=${width}&q=${q}`;
}
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
},
};
export default nextConfig;
Allow external images safely
If an optimized image comes from an external host, allow it with a narrow remotePatterns entry:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '',
pathname: '/products/**',
},
],
},
};
Specify the protocol, hostname, port, pathname, and search pattern when they matter. Omitted fields act as broad wildcards. The older domains option is deprecated since Next.js 14 and does not constrain protocol, port, or pathname, so prefer remotePatterns.
Query-string restrictions
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
pathname: '/assets/**',
search: '?v=*&tenant=public',
},
],
},
};
module.exports = nextConfig;
Do not use a broad wildcard for user-controlled hosts. A narrow allowlist reduces accidental proxying of arbitrary URLs and makes cache behavior easier to reason about.
Quality, widths, and responsive output
Set quality on an individual image when you need a particular trade-off:
<Image
src="/photos/hero.jpg"
alt="Mountain landscape"
width={1600}
height={900}
sizes="(max-width: 768px) 100vw, 80vw"
quality={75}
/>
Current Next.js Image documentation says the images.qualities allowlist is required beginning with Next.js 16:
const nextConfig = {
images: {
qualities: [50, 75, 90],
},
};
module.exports = nextConfig;
When a component requests a quality that is not allowed, Next.js uses the closest allowed value. A direct optimization API request with an unlisted quality returns HTTP 400. Check the installed Next.js version before applying this configuration.
Use sizes for responsive layouts. Without an accurate value, the browser may download an image larger than the rendered slot. Keep the width list and provider’s supported widths aligned where possible.
Provider URL design
Before writing a loader, answer these questions from the provider’s documentation:
- Does the source URL go in a query parameter, path segment, or signed token?
- What parameter represents width?
- How are quality, format, fit, crop, and DPR expressed?
- Does the service require an account ID, zone, or transformation preset?
- Are source URLs required to be publicly reachable?
- Are URLs signed, and which values must be included in the signature?
- Which output formats are supported?
The official Next.js configuration reference includes integration examples for Akamai, AWS CloudFront, Cloudinary, Cloudflare, Contentful, Fastly, Gumlet, ImageEngine, Imgix, PixelBin, Sanity, Sirv, Supabase, Thumbor, ImageKit, and Nitrogen AIO. These examples document integration patterns; they are not a current pricing or feature ranking.
Signed URL example
import crypto from 'node:crypto';
export default function signedLoader({ src, width, quality }) {
const q = quality ?? 75;
const path = `/image?src=${encodeURIComponent(src)}&w=${width}&q=${q}`;
const signature = crypto
.createHmac('sha256', process.env.IMAGE_CDN_SECRET)
.update(path)
.digest('hex');
return `https://img.example.com${path}&sig=${signature}`;
}
Never expose a signing secret in a client bundle. If signing requires secret material, generate the URL on the server or use the provider’s supported server-side integration.
Authentication and protected sources
The default Next.js optimizer does not forward request headers when it fetches a source image. If the origin requires authentication, consider unoptimized, a server-side proxy that adds credentials, or a provider that can fetch the source with an approved authentication method.
<Image
src="https://private.example.com/avatar.jpg"
alt="Account avatar"
width={96}
height={96}
unoptimized
/>
Do not put bearer tokens, private cookies, or long-lived credentials in a public image URL.
Complete example with a CDN-backed gallery
// app/gallery/page.tsx
import Image from 'next/image';
const cdnLoader = ({ src, width, quality }) => {
const params = new URLSearchParams({
src,
w: String(width),
q: String(quality ?? 75),
auto: 'format',
});
return `https://img.example.com/transform?${params}`;
};
const photos = [
{ src: 'https://origin.example.com/photos/forest.jpg', alt: 'Forest' },
{ src: 'https://origin.example.com/photos/coast.jpg', alt: 'Coast' },
];
export default function Gallery() {
return (
<main>
{photos.map((photo) => (
<Image
key={photo.src}
loader={cdnLoader}
src={photo.src}
alt={photo.alt}
width={1200}
height={800}
sizes="(max-width: 700px) 100vw, 50vw"
quality={75}
/>
))}
</main>
);
}
Debugging checklist
- Inspect the rendered
srcandsrcsetin browser developer tools. - Open one generated URL directly and check its status, content type, dimensions, and cache headers.
- Log the loader inputs during development, but remove sensitive source URLs and secrets from production logs.
- Confirm the source host matches
remotePatterns. - Compare every generated parameter with the provider’s current API documentation.
- Check the deployed build, because a local
next.config.jschange may not be present in an older deployment.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| “Invalid src prop” | Host, protocol, path, or query does not match remotePatterns |
Add the narrow pattern that matches the real source URL |
| Provider returns 400 | Wrong parameter names, unsupported width/quality, or malformed encoding | Open the generated URL and reproduce the provider’s documented format exactly |
| Image is blurry | Requested width is too small or sizes describes the layout incorrectly |
Set intrinsic dimensions correctly and provide an accurate sizes value |
| Image is unexpectedly large | Missing or inaccurate sizes, or provider ignores width |
Inspect srcset, add sizes, and verify the provider’s width parameter |
| Private image fails | Origin requires headers that the default optimizer does not forward | Use unoptimized or a server-side authenticated proxy |
| Quality request returns 400 | Quality is not in images.qualities on a version that enforces the allowlist |
Add the quality or request one that is allowed |
| Loader works locally but not after deployment | Wrong project-root-relative loaderFile path or stale build |
Verify the path, export, and deployment build output |
| Generated URL contains a broken source | Source was not encoded before being placed in a query string | Use encodeURIComponent or URLSearchParams |
Performance and reliability
- Use the provider’s width and format transformations so the browser receives only what the layout needs.
- Set
sizesfor responsive images and reserve dimensions withwidth/heightor a known aspect ratio to reduce layout shifts. - Keep URL generation free of network calls. The loader should construct a string, not fetch the image.
- Use stable URLs so the provider and browser can cache variants. Avoid adding timestamps unless cache busting is intentional.
- Monitor provider errors, origin fetch failures, and cache misses separately. A successful Next.js render does not guarantee that the external service transformed the image correctly.
- Keep remote patterns narrow to avoid accidental requests to unsupported or hostile origins.
Next.js documentation does not establish a universal speed improvement for custom loaders. Measure your selected provider with your own image sizes, regions, cache state, and traffic pattern before making performance claims.
Cost considerations
A custom loader changes where image transformation and bandwidth costs occur. Review the provider’s pricing dimensions, including transformation operations, output bandwidth, storage, cache egress, origin requests, and signed-URL features. Next.js configuration alone does not reveal those costs.
Or skip the browser setup
If your goal is to capture a rendered page for documentation, visual checks, or an AI workflow, ScreenshotNeo returns a screenshot or PDF from one request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for the complete option list. It supports full-page and element captures, custom CSS and JavaScript, device presets, dark mode, waiting rules, request blocking, headers and cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, PDFs, and an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.
There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Does a custom loader optimize the image?
No. It generates the URL. The external service represented by that URL performs or serves the transformation.
Can I use different loaders in one application?
Yes. Use a project-wide loader for the default and a per-image loader prop for provider-specific exceptions.
Should I still configure remotePatterns?
Yes, when using external sources with Next.js image handling. Keep the protocol, host, path, and query restrictions as specific as your application permits.
Why does my provider receive a different width than I expected?
Responsive rendering can request several widths from srcset. Inspect the generated candidates and confirm that your sizes value describes the rendered layout.
Can a loader add arbitrary transformations?
It can encode transformations supported by the target provider, but the provider’s URL grammar and account permissions determine what actually works.
References: Next.js Image Component reference and Next.js images configuration reference.


