ScreenshotNeo

BlogHow-to

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.

By the ScreenshotNeo team1 October 20269 min read

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:

  1. Does the source URL go in a query parameter, path segment, or signed token?
  2. What parameter represents width?
  3. How are quality, format, fit, crop, and DPR expressed?
  4. Does the service require an account ID, zone, or transformation preset?
  5. Are source URLs required to be publicly reachable?
  6. Are URLs signed, and which values must be included in the signature?
  7. 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.

// 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

  1. Inspect the rendered src and srcset in browser developer tools.
  2. Open one generated URL directly and check its status, content type, dimensions, and cache headers.
  3. Log the loader inputs during development, but remove sensitive source URLs and secrets from production logs.
  4. Confirm the source host matches remotePatterns.
  5. Compare every generated parameter with the provider’s current API documentation.
  6. Check the deployed build, because a local next.config.js change 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 sizes for responsive images and reserve dimensions with width/height or 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.