ScreenshotNeo

BlogHow-to

How to Optimize Images in Next.js with next/image and Custom Loaders

Choose the built-in Next.js image optimizer or a custom loader, then tune responsive sizes, remote sources, caching, and LCP loading.

By the ScreenshotNeo team4 October 202610 min read

Direct answer: Start with Next.js’s built-in next/image optimizer unless your deployment or image workflow calls for an external image service. Use a custom loader when you want next/image to build transformation URLs for that service. In either setup, accurate sizes, intrinsic dimensions, and deliberate loading priority help avoid oversized downloads and layout shifts.

This guide covers the App Router and Pages Router patterns. Next.js image APIs and defaults can change, so check the documentation for the version installed in your project before copying configuration.

1. Start with layout and responsive candidates

Give each image meaningful alternative text and its intrinsic width and height. Those dimensions describe its aspect ratio and help the browser reserve space before the image loads; CSS still controls the rendered size.

import Image from 'next/image'

export function ArticleImage() {
  return (
    <Image
      src="/images/feature.jpg"
      alt="A developer reviewing a responsive image layout"
      width={1600}
      height={900}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 70vw, 840px"
      style={{ width: '100%', height: 'auto' }}
    />
  )
}

Treat sizes as a description of the image’s expected rendered CSS width at different viewport widths, not as a list of source-file dimensions. The example says the image occupies the full viewport on small screens, 70% up to 1200px, and at most 840px on wider screens. Match these breakpoints and fractions to your actual layout. An inaccurate value can cause the browser to choose a source that is too large or too small. For responsive layouts, especially with fill, omitting sizes can make the browser assume the image is viewport-wide and download an unnecessarily large candidate. See the Next.js Image Component API and the Pages Router Image reference.

Using fill for a responsive crop

fill positions the image to fill its containing block. Give the parent a defined size and a positioning mode such as relative. Choose objectFit deliberately: cover fills the box and may crop; contain keeps the whole image visible and may leave empty space.

import Image from 'next/image'

export function Hero() {
  return (
    <div className="hero-image">
      <Image
        src="/images/hero.jpg"
        alt="A team working around a table"
        fill
        sizes="(max-width: 768px) 100vw, 50vw"
        style={{ objectFit: 'cover' }}
      />
    </div>
  )
}
.hero-image {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
}

The sizes value is only right if the CSS really makes the image full-width on small screens and half-width on larger screens. The parent’s aspect ratio establishes its height; without a sized parent, a fill image has no useful area to fill.

2. Choose the built-in optimizer or an external service

The default loader routes image requests through Next.js’s Image Optimization API. A custom loader instead returns a URL for an external image service to transform. Next.js documents custom loaders as the route for cloud image services; the framework does not configure that provider, validate its URL scheme, or guarantee its availability.

Consideration Built-in optimizer Custom loader and image service
Transformation control Use Next.js’s supported image options and configuration. Depends on the provider’s URL syntax and supported transformations.
Deployment fit Requires a deployment that supports the Image Optimization API, or the appropriate configuration for your hosting setup. Moves transformation delivery to the selected service; verify its integration and hosting requirements.
Source access Remote sources must match configured patterns. The optimizer does not forward authentication headers to the source. Access, credentials, and origin fetching depend on the provider and the URLs your loader creates.
Formats and caching Configured formats and optimizer cache behavior are controlled by Next.js. Format negotiation, caching, invalidation, and accepted widths depend on the provider.
Operations and cost Account for the capabilities and cost of your Next.js hosting environment. Account for the image service’s pricing, limits, cache behavior, and availability. Check its current documentation.

Choose based on deployment support, source access, the transformations you need, and how you want to manage caching and cost. These provider-specific details cannot be inferred from Next.js’s loader interface. See the loader reference and image configuration reference.

3. Configure a custom loader

A loader is a URL-building function. Next.js supplies the source path, requested width, and optional quality; the loader returns the URL that the image service understands. The framework’s documented example uses quality 75 when no quality is specified. For an app-wide loader, set images.loader to custom and point images.loaderFile at a project-root-relative file that exports a default function.

The following is a provider adapter template, not a vendor-specific integration. Replace buildProviderUrl with the URL format and escaping rules documented by the image service you selected. Do not deploy the placeholder function as if it were a working provider.

// app/image-loader.js (or a project-root file of your choice)
export default function imageLoader({ src, width, quality }) {
  const q = quality ?? 75
  return buildProviderUrl({ src, width, quality: q })
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './app/image-loader.js',
  },
}

module.exports = nextConfig

For one image or a limited set, supply a loader directly instead of changing the app-wide setting:

import Image from 'next/image'

function providerLoader({ src, width, quality }) {
  return buildProviderUrl({ src, width, quality: quality ?? 75 })
}

export function ExternalImage() {
  return (
    <Image
      loader={providerLoader}
      src="/catalog/item.jpg"
      alt="A blue ceramic cup"
      width={1200}
      height={800}
      sizes="(max-width: 700px) 100vw, 50vw"
    />
  )
}

Before relying on a loader, confirm the provider’s accepted source path, width and quality limits, URL encoding requirements, format behavior, and cache rules. Ensure the returned URL is reachable by the browser and that you are not exposing a secret in client-side code. A custom loader changes URL generation; it does not install or configure the external service. Consult the current loader documentation.

4. Allow remote images narrowly

When the built-in optimizer fetches a remote image, restrict allowed sources with remotePatterns. Specify only the protocols, hostnames, paths, and query-string policy your app needs. Broad patterns can permit unintended URLs.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        port: '',
        pathname: '/account123/**',
        // Omit search to allow query strings only when your version's
        // configuration semantics make that appropriate. Otherwise set it
        // to the exact expected query string.
      },
    ],
  },
}

module.exports = nextConfig

images.example.com is a placeholder; replace it with a host you control or trust. Check the installed Next.js version’s configuration reference for the exact remotePatterns syntax and query-string behavior. If the upstream image requires authentication headers, the default optimizer will not forward the browser request’s headers. Next.js advises considering unoptimized for such cases, or arranging an appropriate authenticated image delivery path.

SVGs are generally served unoptimized. If you enable SVG optimization, follow Next.js’s documented content disposition and content security policy protections. Do not treat arbitrary remote SVGs as safe merely because they are displayed through an image component.

5. Tune format and caching intentionally

The built-in optimizer can negotiate configured output formats using the request’s Accept header. WebP is the documented default format; AVIF can be enabled, with encoding and cache tradeoffs described in the Next.js documentation. If a proxy or CDN sits in front of Next.js, it must forward Accept for format negotiation to work as intended.

// next.config.js
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 60,
  },
}

module.exports = nextConfig

This example is configuration syntax, not a universal cache recommendation. minimumCacheTTL is a lower bound; upstream cache-control can make the effective cache lifetime longer. The documentation describes no cache invalidation mechanism for generated images. If you replace an image at the same source URL, use a versioned or changed URL where possible, or clear the relevant image cache using the facilities of your deployment. A custom loader delegates output formats and cache behavior to the external provider.

Review the Next.js caching and format documentation before changing production settings; defaults and supported configuration may differ by version.

6. Prioritize the likely LCP image

Preload only when you know which above-the-fold image is likely to be the Largest Contentful Paint (LCP) element. Depending on the Next.js version and router, use the documented preload option or the appropriate eager-loading and fetch-priority controls. The Pages Router reference notes that loading="eager" or fetchPriority="high" may be preferable in most cases. Avoid preloading multiple responsive candidates or an image whose visibility depends on the viewport when another loading control is already in use.

<Image
  src="/images/home-hero.jpg"
  alt="A mountain landscape at sunrise"
  width={1600}
  height={900}
  sizes="100vw"
  fetchPriority="high"
/>

Use only the property supported by your installed Next.js version and verify which image is actually rendered as LCP for the page’s common layouts. Do not give high priority to every image; doing so can compete with other resources. See the Pages Router preload guidance.

7. Troubleshoot common problems

Symptom Likely cause What to check or change
Remote image is rejected or returns an optimizer error The URL does not match remotePatterns, or the pattern’s protocol, hostname, path, or search policy is wrong. Compare the exact source URL with the configured pattern. Narrowly add the intended host and path, then check the installed version’s config reference.
Authenticated remote image fails through the optimizer The default optimizer does not forward authentication headers to the source. Use an image delivery path that does not require forwarded browser credentials, or consider the documented unoptimized behavior where appropriate.
Custom loader returns a broken URL The loader’s URL syntax, source encoding, width, quality, or query construction does not match the provider. Compare a generated URL with the provider’s documented examples. Test special characters, accepted widths, quality bounds, and a direct browser request.
Image downloads are larger than expected sizes is missing or does not match the rendered CSS width; the chosen candidate or transformation is too large. Describe the real layout breakpoints in sizes and inspect the selected srcset candidate in browser developer tools.
Layout shifts when the image appears Intrinsic dimensions or the fill parent’s dimensions are missing or inconsistent. Set accurate width/height, or give a fill parent a definite size or aspect ratio and positioning context.
Fill image is invisible or oddly cropped The parent has no useful dimensions, or fit behavior is not suitable. Give the parent a width and height/aspect ratio, position it, then choose cover or contain intentionally.
AVIF/WebP output is not selected The request’s Accept header is not forwarded, the format is not configured, or a cache/proxy serves a different variant. Check Next.js image configuration and ensure the proxy forwards Accept. Custom-loader format behavior is provider-specific.
Updated source still shows an old optimized image The generated result is cached and the built-in optimizer has no documented invalidation mechanism. Change/version the source URL or clear the deployment’s relevant cache. Review upstream cache-control and TTL settings.
Preloaded image does not match the visible hero The likely LCP asset changes by viewport, or preload conflicts with another loading strategy. Check the actual rendered candidate and LCP on each layout. Preload only a known above-the-fold image; otherwise use appropriate loading or fetch-priority controls.

8. Performance, reliability, and cost checklist

  • Keep source images sensible. Responsive transformations cannot recover detail absent from a source image, and oversized originals can increase origin transfer or processing work. Choose source dimensions that suit the largest intended rendering.
  • Describe rendered width accurately. Tune sizes to the CSS layout so the browser can pick a suitable candidate.
  • Limit allowed origins. Keep remote-source patterns narrow and make upstream availability part of your reliability plan.
  • Plan for cache behavior. Understand the built-in optimizer’s TTL and lack of invalidation, or the selected provider’s cache and purge behavior.
  • Measure the actual page. Compare rendered image dimensions, transferred bytes, layout stability, and LCP for representative viewports and networks. Results depend on your images, CSS, deployment, and cache state; no benchmark is implied here.
  • Model service costs from real usage. Check your host’s image optimization limits and billing, or the external service’s current pricing, transformation limits, and cache policy. Next.js’s loader documentation does not establish vendor prices.
  • Keep the service optional where appropriate. Ensure the app’s behavior for failed image requests is acceptable, and consider whether a direct source or static asset is a suitable fallback for critical content.

Or skip the browser setup

If you need a screenshot of a page while documenting or reviewing an image layout, ScreenshotNeo is a website screenshot API and MCP server. It does not optimize images for next/image or replace a custom image loader; it captures web pages as images or PDFs. One GET request returns a screenshot:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

FAQ

How do I use a remote image with next/image?

With the built-in optimizer, allow the exact remote source using remotePatterns. If you use a custom loader, follow the image provider’s documented source URL format instead.

What should I put in sizes?

Describe the image’s rendered CSS width at relevant breakpoints. Derive it from the actual layout, including maximum widths and columns; do not copy a generic example without checking the CSS.

When should I preload an image?

Only when you know the above-the-fold image likely to be LCP. If that varies by viewport, or another loading control is more suitable, avoid a preload that may fetch the wrong candidate.

Does a custom loader change the image component API?

No. It changes how the source URL is built. The provider’s URL rules, transformations, formats, caching, and availability remain provider-specific.