On this page

Chapter 4 of 8. Prerequisite: Chapter 2, where images.unoptimized: true first appeared in the config. This chapter explains why that line has to be there, so the next time a build dies on <Image> you know the cause in ten seconds instead of an afternoon.

You add a next/image component, run next build, and the build stops with an image-optimisation error. Or you run next dev and hit the same wall a moment sooner. Both failures point at one thing, and the fix is one config key. But the key only sticks if you understand what next/image is reaching for, because the workaround changes what you get back.

What next/image does that a static export can't

The <Image> component is not a styled <img>. On a normal Next.js deployment it routes every image request through a running optimisation endpoint. That endpoint resizes the source to the size the device needs, re-encodes it to a modern format like WebP or AVIF when the browser accepts one, then caches the result. The src, width, and quality you pass the component become query parameters on a URL the server answers on demand, per request.

That is the whole problem in one sentence: it needs a server. A static export has none. Chapter 1 of this course drew the line. Setting output: 'export' produces a folder of flat files with no Node process behind them, and a cPanel shared host running LiteSpeed serves those files without ever running your application code. There is nothing to answer an on-demand optimisation request, so the default loader has nothing to talk to.

Next.js knows this and refuses to pretend. The static-export guide lists "Image Optimization with the default loader" among the "Unsupported Features" for output: 'export'. That is why the build fails rather than shipping a broken image endpoint. The failure is the framework doing you a favour, catching the mismatch at build time instead of at 404-time in production.

The one-line fix: images.unoptimized

Set one key in next.config.ts and the build completes:

const nextConfig: NextConfig = {
  output: 'export',
  images: {
    unoptimized: true,  // Required for static export — Next image optimisation needs a server
  },
}

That comment is the verbatim one from this site's config (captainrandom.co.uk, in production). The images config reference documents unoptimized as the global switch, and the Image component reference describes the per-image unoptimized prop as "A boolean that indicates if the image should be optimized," defaulting to false.

Here is what the flag does. It tells Next to skip the optimisation pipeline and emit each <Image> as an <img> pointing straight at the source file copied into out/. Nothing resizes, nothing re-encodes, nothing generates a per-device variant. The image ships exactly as it sits on disk. You keep the ergonomics of the component (the width/height layout reservation, the priority and loading props, the same JSX everywhere) and drop only the server-side transform.

Setting it globally in next.config.ts is the right move for a static export because every image is affected by the same constraint. There is no server for any of them. You could instead pass unoptimized on individual <Image> instances, but under output: 'export' that just invites the one you forgot to break the build. Set it once, globally, and be done.

What "unoptimized" costs you

Be honest with yourself about the trade, because it is real. With optimisation off you lose three things:

  1. Automatic resizing. A 3000px-wide source is sent to a phone at full resolution. The browser scales it down for display, but it downloaded every one of those bytes first.
  2. Format conversion. No automatic WebP or AVIF. A PNG stays a PNG; a JPEG stays a JPEG.
  3. Per-device variants. No generated srcset, so no "right size per screen" selection.

That third point is worth pinning down. The Image component docs note that when you pass a src, "both the srcset and src attributes are generated automatically for the resulting <img>," and that the sizes prop steers how wide that generated set goes: without it, Next emits "a limited srcset (e.g. 1x, 2x)"; with it, "a full srcset (e.g. 640w, 750w, etc.), optimized for responsive layouts." Turn optimisation off and Next stops generating those candidates for you. The sizes hint has nothing left to select from.

The fix therefore moves the optimisation work to you, at authoring time, before the file ever enters the repo. Export your hero art at the dimension it renders at, not at camera resolution. Run it through a compressor. Reach for AVIF or WebP by hand if the browser support you care about allows it. Then the file that lands in out/ is already the file you'd want served, and there is nothing left for a runtime optimiser to do.

One thing you don't lose is layout stability, because that lives on the component, not the optimiser. The docs describe width and height as the values "used to infer the correct aspect ratio used by browsers to reserve space for the image and avoid layout shift during loading." Setting unoptimized: true leaves those attributes in place, so your CLS budget is untouched.

When a build-time export optimiser is worth it

unoptimized: true is the default answer and the one this site ships. But it is not the only path. For an image-heavy static site there's a better one that keeps next/image doing real work.

The static-export guide states plainly that "Image Optimization through next/image can be used with a static export by defining a custom image loader in next.config.js." A custom loader is a function that, per the images config reference, receives { src, width, quality } and returns a URL string. You wire it in with two keys:

const nextConfig: NextConfig = {
  output: 'export',
  images: {
    loader: 'custom',
    loaderFile: './my-image-loader.ts',
  },
}

The loaderFile "must point to a file relative to the root of your Next.js application," and it "must export a default function that returns a string." The docs ship worked loaders for Cloudinary, Imgix, Cloudflare, ImageKit, and more. Every one follows the same pattern: your loader points next/image at a cloud provider's optimisation URL instead of at Next's own server. Optimisation still happens, but it happens on a service you don't run, so the static export is fine. In return you keep the resized, reformatted, per-device srcset you'd otherwise lose, and you take on a CDN dependency plus a bill.

The decision comes down to two numbers. If your site has a handful of images you can size and compress by hand at authoring time, unoptimized: true is the right call. There's nothing to install, nothing to pay for, and the optimiser would have had little to do anyway. If you have hundreds of user-supplied or large images where per-device resizing matters, a custom loader pointed at a CDN earns its keep. This site sits in the first camp, which is why the config is one line and not a loader file.

The acceptance threshold

You've handled next/image under static export correctly when all of these hold:

  1. next build completes with no image-optimisation error, and next dev doesn't throw one either. (The wording changes across versions; the tell is any message that says image optimisation is unavailable or incompatible with export.)
  2. Every <Image> in the built out/ folder renders as a plain <img> pointing at a file that exists in out/, with no /_next/image?url=... optimisation URLs anywhere in the HTML.
  3. Your source images are sized and compressed at authoring time (if you took the unoptimized path), or your custom loader returns valid provider URLs (if you took the loader path).
  4. The site's images load from the cPanel file host with no application server involved.

If you took the one-line fix, that's the whole chapter: images.unoptimized: true, and move your image budget to authoring time. Chapter 5 is the next wall, and a bigger one. It's the .htaccess that makes deep links and refreshes resolve on cPanel/LiteSpeed, where the obvious SPA rewrite rule turns out wrong for a trailing-slash export.

// EXERCISE

Move your image budget to authoring time and guard it

On your own project, do the work the optimiser used to do. Take your three heaviest images, resize each to the largest dimension it actually renders at, re-encode to WebP or AVIF where your browser support allows, then add a guard script that fails the build when an oversized image or an optimisation URL sneaks back in.

Expected behaviour
  • Before and after byte sizes recorded for each of the three images, with the rendered dimension used as the resize target
  • The built HTML in out/ contains no /_next/image?url= optimisation URLs anywhere
  • Every img src in the exported HTML points at a file that actually exists inside out/
  • A guard script fails the build when any image in out/ exceeds a byte budget you set, and passes on the current image set

PROVE IT Drop a full camera-resolution image into a page, rebuild, and capture the guard failing on it, then resize the file and capture the passing run.

// CHECKPOINT — IMAGES UNDER EXPORT
multiple choice · auto-checked

Why does the default next/image loader fail under output: 'export'?

exact answer · auto-checked

Alongside loader: 'custom', which config key names the file that exports your loader function?

open · self-checked

You have set images.unoptimized: true. What in the built out/ folder proves next/image was handled correctly?

Show answer

Every Image in the built HTML renders as a plain img pointing at a file that actually exists in out/, with no /_next/image?url= optimisation URLs anywhere in the markup. The images then load from the file host with no application server involved. The sources themselves should already be sized and compressed at authoring time, because that is where the optimisation work moved.

↺ re-read: “The acceptance threshold

Lived experience

Sources

  • How to create a static export of your Next.js application
    Next.js (Vercel)
    Lists Image Optimization with the default loader among the features static export forbids, documents the custom-loader path that keeps next/image working under output:'export', and states that using an unsupported feature with next dev raises an error similar to setting dynamic:'error'.
    nextjs.org
  • next.config.js Options: images
    Next.js (Vercel)
    Documents images.unoptimized and the custom loader configuration (loader:'custom' + loaderFile pointing to a file relative to the app root that exports a default function returning a string), including the note that a loader file requires Client Components ('use client') to serialize the function, plus worked provider examples.
    nextjs.org
  • Image Component (next/image)
    Next.js (Vercel)
    Reference for the unoptimized prop ('A boolean that indicates if the image should be optimized,' default false); the cases that 'do not benefit from optimization' (small images under 1KB, SVG, animated GIF, with SVG skipped automatically when src ends in .svg); the automatically generated srcset and how the sizes prop widens it (limited 1x/2x without sizes, full 640w/750w set with it); and width/height inferring the aspect ratio browsers use to reserve space and avoid layout shift.
    nextjs.org
  • LiteSpeed Web Server
    LiteSpeed Technologies
    Independent reference for the serving layer under a cPanel static export: LiteSpeed Web Server 'can be used as a drop-in replacement for an Apache web server,' i.e. it serves the flat files in out/ directly with no application server behind them — the environment that leaves nothing to answer an on-demand optimisation request.
    docs.litespeedtech.com
Back to guide overview