Skip to content

Fix: Next.js Image Optimization Errors – Invalid src, Missing Loader, or Unoptimized

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Next.js Image component errors including 'Invalid src prop', 'hostname not configured', missing loader, and optimization failures in production.

next/image Optimization Errors

The first time I hit a next/image error I treated it as one problem and reached for one fix, usually slapping unoptimized on the component until the red overlay went away. That instinct is exactly wrong: these errors come from at least four unrelated subsystems, and the message text tells you which one. I learned to read the actual string first, because “hostname not configured” and “sharp is required” have nothing to do with each other, and the fix for one does nothing for the other. In my experience the single most useful question is whether it failed in dev, at build, or only in production.

You use the Next.js <Image> component and hit one of these errors at build time or in the browser console:

Error: Invalid src prop (https://example.com/photo.jpg) on `next/image`, hostname "example.com" is not configured under images in your `next.config.js`
Error: Image with src "/hero.png" must use "width" and "height" properties or "fill" property.
Error: Image Optimization using the default loader is not compatible with `next export`.

You may also see these related variants:

Error: Unable to optimize image and target URL is malformed
Error: 'images.remotePatterns' configuration is required to use external images
Module not found: Can't resolve 'sharp'
Warning: Image with src "..." was detected as the Largest Contentful Paint (LCP). Please add the "priority" prop to the Image component.

In development these errors usually appear as a red overlay or console warning. In production, images may fail to load entirely, display as broken thumbnails, or silently fall back to unoptimized versions that hurt your Core Web Vitals scores.

What the Image Pipeline Enforces

The Next.js <Image> component is not a simple <img> tag. It wraps a full image optimization pipeline that resizes, converts, and caches images on the fly. To do this safely, Next.js enforces several constraints:

  1. External hostnames must be explicitly allowed. Next.js refuses to optimize images from domains you haven’t whitelisted. This prevents your server from being used as an open proxy to fetch and process arbitrary URLs.
  2. Width and height are required. The browser needs to know the image dimensions before it loads so it can reserve the correct amount of space and avoid layout shift (CLS). Next.js requires you to declare these dimensions upfront or use the fill prop.
  3. The default image loader requires a running server. When you use next export (or output: 'export' in next.config.js) to produce a static site, there is no server to handle on-the-fly image optimization. You need a custom loader or must mark images as unoptimized.
  4. The sharp package must be present at runtime. Since Next.js 14.2, sharp is the optimizer in both development and production and ships as an automatically-installed optional dependency (the old squoosh WASM fallback was removed). You rarely install it by hand anymore, but optional dependencies get stripped by --omit=optional, trimmed Docker stages, or a build-vs-runtime architecture mismatch, and then optimization throws. On Next.js older than 14.2, squoosh is the dev fallback and sharp has to be installed manually for production.

Each fix below addresses a specific cause. Most projects will need to apply more than one.

Diagnostic Timeline

Here is the order an experienced Next.js developer actually walks through these errors in a real project, including the wrong moves that teams reach for first and how to discard them.

Minute 0, first reaction: “install sharp.”

You install sharp, redeploy, and the same error returns. Discard this hypothesis if the error message is “hostname not configured” or “Invalid src prop”, those are config-side errors that sharp cannot fix. sharp only matters when the message explicitly names it or the optimizer fails at runtime. Since 14.2, sharp is an auto-installed optional dependency (there is no more squoosh fallback), so a missing sharp almost always means something stripped optional dependencies in your build or container, not that you forgot to add it. Skip this for now and read the actual error string.

Minute 2, second reaction: “add unoptimized={true}.”

Tempting because the error vanishes immediately, but this is the worst long-term fix. It masks the real cause and disables the entire Next.js image pipeline for that component, no srcset, no AVIF/WebP conversion, no LCP preload. If unoptimized makes the error go away, you have proven that the optimizer is the failing part; now you have to figure out why, not whether to keep optimizing.

Minute 5, third reaction: “the wrong hostname is whitelisted.”

You check next.config.js and the remotePatterns array contains the CDN you expect. Print the config at build time to confirm it is actually being read:

// next.config.js
const config = { images: { remotePatterns: [...] } };
console.log('IMAGES CONFIG:', JSON.stringify(config.images, null, 2));
module.exports = config;

If you do not see the log line during npm run build, the config file is not being loaded, you have a syntax error, the wrong filename (next.config.mjs vs next.config.js), or a Vercel project setting overriding it.

Minute 8, actual root cause.

Three patterns account for nearly every production-only image failure:

  • output: 'export' is set in next.config.js. Static export does not run a Node.js server, so /_next/image simply does not exist at runtime. You must either remove the export, set images.unoptimized: true (accepting the trade-off), or wire a custom loaderFile that points to a CDN-side optimizer (Cloudflare Image Resizing, imgix, Cloudinary).

  • The deploy platform does not support the default loader. Cloudflare Pages, Netlify (without @netlify/plugin-nextjs), AWS Amplify hosting in some modes, and any static-only host fall into this bucket. Symptom: images load locally and on Vercel preview deploys but break the moment you deploy to your real host. Audit next.config.js for loader and loaderFile, and check your platform’s docs for the required adapter. The same class of host-incompatibility issue shows up at the platform level, see Fix: Cloudflare Pages Not Working.

  • sharp’s optional dependency got stripped. On Next.js 14.2+ sharp rides in as an optional dependency of next, so it disappears when an install runs --omit=optional, when a Docker stage copies node_modules without sharp’s native binary, or when the build and runtime architectures differ. (On older Next.js the equivalent mistake is putting a manually-added sharp in devDependencies, which --omit=dev then skips.) Verify it actually loads at runtime:

node -e "console.log(require('sharp').versions)"

Run this inside the production container or after your production install. If it throws Cannot find module 'sharp', pin it as a direct dependency so it is never treated as optional:

npm install sharp

If the error names a hostname instead of an optimizer failure, the fix path is remotePatterns (Fix 1). If it complains about width/height, the fix is on the component (Fix 2). The diagnostic question, “did this fail in build, in dev, or only in production?”, narrows the cause faster than any other check.

Fix 1: Configure remotePatterns for External Images

When you load an image from an external URL, Next.js blocks it unless you whitelist the hostname. The older domains array has been deprecated since Next.js 14 (it logs a warning and is being phased out, so treat it as legacy only) in favor of remotePatterns, which gives you finer-grained control over protocols, ports, and pathname patterns. Use remotePatterns for anything new.

Broken code:

import Image from 'next/image';

function Avatar({ user }) {
  return (
    <Image
      src={`https://cdn.example.com/avatars/${user.id}.jpg`}
      alt={user.name}
      width={80}
      height={80}
    />
  );
}

This throws the “hostname not configured” error because cdn.example.com is not in your config.

Fix, add remotePatterns to next.config.js:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/avatars/**',
      },
      {
        protocol: 'https',
        hostname: '*.amazonaws.com',
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
};

module.exports = nextConfig;

Each entry in remotePatterns specifies which protocol, hostname, port, and pathname pattern are allowed. The ** wildcard matches any number of path segments. You can use * in hostnames to match subdomains (e.g., *.amazonaws.com matches my-bucket.s3.amazonaws.com).

If you still have a deprecated domains array, here is what it looked like and what it maps to:

// Deprecated since Next.js 14 — migrate this to remotePatterns
images: {
  domains: ['cdn.example.com', 'images.unsplash.com'],
},

The domains option only matches hostnames. It can’t restrict by protocol, port, or path, which is exactly why it was deprecated. Move any remaining domains entries to remotePatterns before they stop working in a future major. If you have environment variables controlling your CDN hostname, make sure they are available at build time since next.config.js runs during the build.

One habit to avoid: whitelisting every CDN domain with a blanket pathname: '/**'. That hands back most of the protection the allowlist exists to give you. Pin the pathname as tightly as the real URLs allow, so your /_next/image endpoint cannot be turned into an open proxy that fetches and resizes arbitrary images on your bill.

Fix 2: Provide width and height Props

The <Image> component requires explicit dimensions so the browser can allocate space before the image loads. Without them, the page layout shifts when the image appears, causing poor CLS scores.

Broken code:

<Image src="/hero.png" alt="Hero banner" />

Fix, add width and height:

<Image src="/hero.png" alt="Hero banner" width={1200} height={630} />

The values should match the intrinsic dimensions of the image (or the aspect ratio you want). Next.js uses these to calculate the correct srcset sizes. The rendered size is still controlled by CSS, the width and height props just define the aspect ratio and tell the optimization pipeline what dimensions to generate.

If you don’t know the exact dimensions ahead of time (e.g., user-uploaded images), you have two options:

Option A, use the fill prop (see Fix 3 below).

Option B, import the image statically:

import heroImage from '../public/hero.png';

// Next.js automatically reads the width and height from the file
<Image src={heroImage} alt="Hero banner" />

Static imports work for images in your project’s file system. Next.js reads the file at build time and injects the dimensions automatically. This is the simplest approach when the image is part of your codebase.

Fix 3: Use the fill Prop for Dynamic or Unknown Dimensions

When the image dimensions are not known at build time, for example, images from a CMS, user uploads, or an API, use the fill prop instead of width and height.

Fix:

function CoverImage({ url, altText }) {
  return (
    <div style={{ position: 'relative', width: '100%', height: '400px' }}>
      <Image
        src={url}
        alt={altText}
        fill
        style={{ objectFit: 'cover' }}
      />
    </div>
  );
}

When you use fill, the image expands to fill its parent container. The parent must have position: relative, position: absolute, or position: fixed so the absolutely positioned image has a reference frame. Set the container’s dimensions with CSS to control how large the image appears.

The style={{ objectFit: 'cover' }} tells the browser how to crop the image if its aspect ratio doesn’t match the container. Other options include contain (scales down to fit without cropping) and fill (stretches to fill, possibly distorting).

You should also provide a sizes prop when using fill so Next.js can generate the right srcset:

<Image
  src={url}
  alt={altText}
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  style={{ objectFit: 'cover' }}
/>

Without sizes, Next.js assumes the image could be as wide as the viewport, which may cause it to serve unnecessarily large files on desktop layouts where the image only takes up a fraction of the screen. This is one of those subtle issues that doesn’t produce an error but silently degrades performance, similar to how a misconfigured useEffect dependency array can silently degrade rendering performance.

Fix 4: Handle Static Export with a Custom Loader

When you use output: 'export' in your Next.js config (or the legacy next export command), there is no Node.js server at runtime. The default image loader can’t run because it needs a server to process images on the fly.

Error:

Error: Image Optimization using the default loader is not compatible with `next export`.

Fix Option A, use unoptimized globally:

// next.config.js
const nextConfig = {
  output: 'export',
  images: {
    unoptimized: true,
  },
};

module.exports = nextConfig;

This disables all image optimization. Images are served as-is. This is the simplest fix, but you lose all the performance benefits of Next.js image optimization.

Fix Option B, use a custom loader that points to an external optimization service:

// next.config.js
const nextConfig = {
  output: 'export',
  images: {
    loader: 'custom',
    loaderFile: './image-loader.js',
  },
};

module.exports = nextConfig;
// image-loader.js
export default function cloudflareLoader({ src, width, quality }) {
  const params = [`width=${width}`, `quality=${quality || 75}`, 'format=auto'];
  return `https://your-domain.com/cdn-cgi/image/${params.join(',')}/${src}`;
}

With a custom loader file, the <Image> component still generates srcset with multiple sizes, but instead of pointing to /_next/image, it points to your external image CDN. The optimization happens at the CDN level, not on a Next.js server.

You can also set a loader per image instead of globally:

const imgixLoader = ({ src, width, quality }) => {
  return `https://your-account.imgix.net${src}?w=${width}&q=${quality || 75}&auto=format`;
};

<Image
  loader={imgixLoader}
  src="/hero.png"
  alt="Hero"
  width={1200}
  height={630}
/>

Fix 5: Custom Loaders for Cloudflare, Vercel, and Self-Hosted Deployments

Different hosting platforms handle image optimization differently. Using the wrong loader for your platform causes broken images or unnecessary round-trips.

Vercel (default, no configuration needed):

Vercel handles image optimization automatically with the default loader. No extra configuration is required. If you are deployed on Vercel and still seeing errors, check that your next.config.js doesn’t set a custom loader that overrides the default.

Cloudflare Pages:

Cloudflare Pages does not support the default Next.js image optimizer. Use Cloudflare Image Resizing:

// image-loader.js
export default function cloudflareLoader({ src, width, quality }) {
  if (src.startsWith('/')) {
    // relative path — prefix with your domain
    src = `https://your-domain.com${src}`;
  }
  return `https://your-domain.com/cdn-cgi/image/width=${width},quality=${quality || 75},format=auto/${src}`;
}

Self-hosted with Node.js:

When self-hosting, you need the sharp package installed in production:

npm install sharp

If you are using Docker, make sure sharp is not excluded from your production node_modules. A common mistake is using a multi-stage build that copies only certain files and leaves sharp’s native binaries behind:

# Make sure sharp is included in the production stage
FROM node:20-alpine AS runner
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/package.json ./package.json

If sharp fails to install on your target platform, it’s often a native dependency issue. Check that your build platform matches your deployment platform architecture (e.g., don’t build on macOS ARM and deploy to Linux x86). If you see a Cannot find module error referencing sharp, the package is either missing or built for the wrong platform.

Fix 6: Add priority to Above-the-Fold Images

This one is a warning, not an error, but ignoring it hurts your Largest Contentful Paint (LCP) score:

Warning: Image with src "..." was detected as the Largest Contentful Paint (LCP). Please add the "priority" prop.

Fix:

<Image
  src="/hero.png"
  alt="Hero banner"
  width={1200}
  height={630}
  priority
/>

The priority prop tells Next.js to preload this image in the <head> using <link rel="preload">. It also disables lazy loading for this image. Use it on the largest visible image above the fold, typically a hero banner, product image, or featured article thumbnail.

Only one or two images per page should have priority. Adding it to every image defeats the purpose and can actually slow down page loads by competing for bandwidth.

For images below the fold, Next.js automatically sets loading="lazy", which defers loading until the image is near the viewport. You can also set this explicitly:

<Image
  src="/gallery-item.png"
  alt="Gallery item"
  width={400}
  height={300}
  loading="lazy"
/>

The default behavior is already lazy loading for non-priority images, so setting loading="lazy" explicitly is only needed if you want to make the intent clear in your code.

Fix 7: Handle the sharp Dependency in Production

Since Next.js 14.2, sharp (native C libraries, much faster than the old WASM path) is the optimizer in every environment and is pulled in automatically as an optional dependency, squoosh was removed. So you usually do not install sharp yourself; the failure is that something in your pipeline drops the optional dependency or its native binary, and the optimizer then cannot start. In standalone output mode the message is explicit:

Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly.

On older Next.js (before 14.2), the same situation surfaces as a softer warning, “the optional ‘sharp’ package is strongly recommended”, and the build silently falls back to squoosh at full memory cost. Either way, the fix is the same: make sure sharp actually reaches your runtime.

Fix:

npm install sharp

Common issues with sharp installation:

Issue 1, sharp is in devDependencies instead of dependencies:

# Wrong — sharp won't be available in production
npm install --save-dev sharp

# Right
npm install sharp

Issue 2, platform mismatch in CI/CD:

If you build on a Mac but deploy to Linux, sharp’s native binaries won’t work. Force the correct platform during install:

npm install --os=linux --cpu=x64 sharp

Or use the SHARP_IGNORE_GLOBAL_LIBVIPS environment variable to force a fresh build:

SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install

Issue 3, Docker Alpine missing dependencies:

RUN apk add --no-cache libc6-compat

Alpine Linux requires libc6-compat for sharp to work. Without it, the binary loads but crashes with a cryptic error about missing symbols.

If your production environment truly cannot run sharp (e.g., edge runtimes, serverless platforms with size limits), use a custom loader to offload optimization to an external service (see Fix 4).

Fix 8: Fixing next/image in the App Router vs. Pages Router

The <Image> component works the same way in both routers, but how you configure it differs slightly. A common mistake is putting the configuration in the wrong place or using outdated syntax.

App Router (Next.js 13+), the import is the same:

import Image from 'next/image';

Pages Router, also the same import, but watch out for the legacy import:

// Correct
import Image from 'next/image';

// Legacy — still works but deprecated
import Image from 'next/legacy/image';

The legacy next/legacy/image component has a different API. It uses layout prop (layout="fill", layout="responsive") instead of the fill prop and style prop. If you’re following a tutorial written for Next.js 12 and using Next.js 13+, you’ll see errors because the props have changed.

Migration from legacy to current:

// Next.js 12 (legacy)
<Image src="/photo.jpg" alt="Photo" layout="fill" objectFit="cover" />

// Next.js 13+ (current)
<Image src="/photo.jpg" alt="Photo" fill style={{ objectFit: 'cover' }} />
// Next.js 12 (legacy)
<Image src="/photo.jpg" alt="Photo" layout="responsive" width={800} height={600} />

// Next.js 13+ (current)
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  style={{ width: '100%', height: 'auto' }}
/>

If you’re upgrading a project and see unfamiliar prop errors, check whether your code is using the legacy component. The two codemods run in opposite directions, so pick by where you are starting:

# Coming straight from Next.js 12: pin everything to the legacy component first
npx @next/codemod next-image-to-legacy-image .

# Then modernize legacy -> current (this is the one that fixes the prop errors above).
# Experimental: covers static usage, not dynamic <Image {...props} />.
npx @next/codemod next-image-experimental .

If your code already imports next/legacy/image, you only need the second codemod, the first is just the Next.js 12-to-13 holding step.

If modules fail to resolve during the codemod, you may be hitting a module resolution issue, make sure your node_modules are installed and up to date.

When Images Still Will Not Optimize

Verify Your Config Is Being Read

A surprisingly common mistake is editing the wrong next.config.js or having a syntax error that silently prevents the config from loading. Add a temporary console.log to verify:

// next.config.js
console.log('Config loaded:', JSON.stringify(nextConfig.images, null, 2));
module.exports = nextConfig;

Run npm run dev and check the terminal output. If you don’t see your images config printed, the file is not being read.

Clear the Image Cache

Next.js caches optimized images in .next/cache/images. If you’ve changed your config but images are still broken, clear the cache:

rm -rf .next/cache/images

Or delete the entire .next directory and rebuild:

rm -rf .next
npm run build

Check for Conflicting Middleware

If you have Next.js middleware that rewrites or redirects image requests, it can interfere with the /_next/image endpoint. Make sure your middleware matcher excludes image optimization routes:

// middleware.js
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Test with a Minimal Example

If the error persists, create a minimal reproduction to isolate the issue. Strip your component down to just the <Image> tag with a local static image:

import Image from 'next/image';
import testImg from '../public/test.jpg';

export default function TestPage() {
  return <Image src={testImg} alt="Test" />;
}

If this works, the problem is with your specific src URL, configuration, or loader. Add back complexity one piece at a time until the error reappears.

New errors after upgrading to Next.js 16

Next.js 16 (October 2025) tightened image security, so several setups that were fine on 15 start erroring the moment you upgrade. If your images broke right after a major-version bump, check these first:

  • quality values must be whitelisted now. The default images.qualities changed from “any value allowed” to just [75]. A component with quality={90}, or a direct hit to /_next/image?...&q=90, is now rejected (the REST endpoint returns 400). Declare the values you actually use:
// next.config.js
images: { qualities: [50, 75, 90] }
  • Local images with a query string need localPatterns. A src like /assets/logo.png?v=2 is now blocked unless you allow it explicitly, which closes an enumeration vector:
images: {
  localPatterns: [{ pathname: '/assets/**', search: '?v=2' }],
}
  • Redirects are capped. images.maximumRedirects now defaults to 3 (it was unlimited), so an external image sitting behind a long redirect chain can fail where it used to resolve.

These are deliberate hardening changes, so the fix is to configure the allowlists, not to switch optimization off.

Check Your Hosting Platform’s Documentation

Every hosting platform handles image optimization differently. Vercel supports it natively. Netlify requires the @netlify/plugin-nextjs plugin. AWS Amplify has its own limitations. Cloudflare Pages needs a custom loader. Docker deployments need sharp. Read your platform’s specific Next.js documentation, the default configuration rarely works everywhere without adjustment.

If your images load in development but break in production, the issue is almost always the loader or a missing sharp dependency. Check your deployment logs for warnings about image optimization. If you’re seeing environment-related issues where config values aren’t available at runtime, see Fix: Environment Variable is Undefined for common causes. And if your React components unmount unexpectedly during navigation, that can also cause image loading to abort mid-request.

Images 404 with /_next/image?url=... after a hosting platform change

If you moved a project from Vercel to another host (Cloudflare Pages, Netlify, self-hosted) and image URLs now return 404, the platform is not running the /_next/image optimizer route. The HTML still references /_next/image?url=...&w=...&q=75 because the <Image> component generated it at build time. You need a loader that rewrites those URLs to your platform’s image endpoint, or you need to set images.unoptimized: true and accept unoptimized output. Check the platform’s Next.js adapter docs before deploying.

Hydration mismatch when images render different sources on server vs client

If you compute src from window.devicePixelRatio or navigator.userAgent, the server-rendered HTML disagrees with the client and React throws a hydration warning right next to the image. Move the conditional logic into a useEffect and render a placeholder on first paint, or use the sizes prop with media queries so the browser picks the right candidate without JS branching. The general failure mode is documented in Fix: Next.js Hydration Mismatch.

Aborted image requests after a fast route change

Navigating away from a page mid-load can cancel in-flight image requests and surface as a NetworkError in the console. The fix is to mark slow images with loading="lazy" so they never start during a transition, or to debounce navigation. The matching component-state symptom, a setState after unmount because the image’s onLoad fired late, is covered in Fix: Can’t perform a React state update on an unmounted component. If you are also seeing infinite re-renders that affect image props, see Fix: React useEffect Infinite Loop.


Related: Fix: Next.js Hydration Mismatch | Fix: useEffect Infinite Loop | Fix: Cannot find module (TypeScript) | Fix: Environment Variable is Undefined | Fix: Cannot update unmounted component

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles