Back to Blog

Next.js Image Optimization Failing on Static Export

Published: July 29, 2026
Next.js Image Optimization Failing on Static Export

Next.js image optimization failing static export shows up in one of two ways: next build throws an explicit error naming the Image Optimization API the moment you set output: 'export', or — if you've already worked around the build failure without fully understanding why — your images quietly ship unoptimized, larger and slower than they should be, with nobody noticing until a Lighthouse score tanks. Both trace back to the same cause. The default next/image loader needs a running server to resize images the moment a browser asks for a specific size, and a static export has no server at all, by definition.

Short answer: the default image loader depends on Next.js's Image Optimization API, which only works with a live server behind it — output: 'export' produces static files with nothing running, so you need either unoptimized: true to skip optimization entirely, or a custom loader that delegates resizing to an external service that already handles it.

A laptop displaying a badly cracked, glitching screen full of color artifacts, representing next/image failing outright once there's no server left to optimize anything

Why the Default Image Optimizer Needs a Server

Next.js's own documentation on this exact error is direct about the mechanism: the default loader relies on the Image Optimization API, and "Next.js optimizes images on-demand, as users request them — not at build time." That's the part that breaks under static export. A server-rendered deployment can resize, re-encode, and cache an image the first time a browser asks for a specific width, then serve that cached version afterward. A static export has no process left running after next build finishes — there's nothing to receive that on-demand request at all, which is exactly why the build fails rather than silently shipping something broken.

(If you've been checking your next/image component's props for something misconfigured — it's very likely fine. The component isn't the problem. The runtime it's quietly assuming exists, isn't there.)

Fix 1: unoptimized: true — Simplest, Honest About the Tradeoff

The direct fix skips the optimizer entirely:

JavaScript
1// next.config.js
2module.exports = {
3  output: 'export',
4  images: {
5    unoptimized: true,
6  },
7};

With this set, next/image renders a plain <img> tag pointing straight at your original image file — no automatic resizing, no format conversion, no generated srcset. The full next/image API reference documents every prop this still preserves even with optimization disabled, like alt and lazy loading via the native loading attribute. It's the right call when your images are already reasonably sized, or when whatever's serving them (a CDN, an image host) handles optimization on its own. It's also the most honest option: you're not pretending to get automatic optimization you're not actually getting.

A vintage brass balance scale with two pans, weighted evenly against each other, representing the tradeoff between the simplicity of skipping optimization and the extra setup of a custom loader

Fix 2: A Custom Loader — Real Optimization From an External Service

If your images come from a service that already does its own resizing — which is exactly the pattern this site uses with Pexels — a custom loader gets you genuine width-specific, format-aware image delivery without needing Next.js's own server-side optimizer at all:

JavaScript
1// next.config.js
2module.exports = {
3  output: 'export',
4  images: {
5    loader: 'custom',
6    loaderFile: './image-loader.js',
7  },
8};
JavaScript
1// image-loader.js — delegates resizing to Pexels's own URL parameters
2export default function pexelsLoader({ src, width, quality }) {
3  const url = new URL(src);
4  url.searchParams.set('auto', 'compress');
5  url.searchParams.set('cs', 'tinysrgb');
6  url.searchParams.set('w', width.toString());
7  url.searchParams.set('q', (quality || 80).toString());
8  return url.toString();
9}

The loader's only job is building a URL string — it never calls anything server-side, so it works identically whether the app is server-rendered or statically exported. Next.js's static export documentation shows the equivalent pattern for Cloudinary if your images live there instead; the mechanism is the same regardless of which CDN's query parameters you're targeting.

An overhead-loaded cargo truck driving fast down a highway with motion blur, representing image resizing responsibility being delegated to an external CDN instead of Next.js's own server

Choosing Between the Two

The honest answer depends entirely on where your images actually live. Local image files with no CDN in front of them get real value from a tool like next-image-export-optimizer, which pre-generates multiple resized versions during next build and serves the right one via a custom loader — more build time and disk space, in exchange for genuine per-size optimization on files that would otherwise ship at one fixed size. Images already served by something that resizes on request — Pexels, Cloudinary, most modern CDNs — get more value from a thin custom loader than from unoptimized: true, since the loader unlocks real width-aware delivery for the cost of one small config file. unoptimized: true is the right default when neither of those applies, or when you'd rather be honest about skipping optimization than half-configure a loader that can't fully replace it anyway.

The Opinion Part

Here's the position worth stating plainly: reaching for unoptimized: true the moment next build throws this error, without checking whether your images already have a CDN capable of real resizing sitting right there, is leaving a genuinely free optimization on the table. This project serves every blog post's images through Pexels specifically because a thin custom loader gets automatic, width-aware, format-aware delivery without needing a server at all — the entire reason a static-export site can still ship correctly sized images per device instead of one oversized file to everyone. The five minutes it takes to check whether your image source already resizes on request is worth spending before defaulting to the option that quietly gives up on optimization altogether.

Conclusion

If Next.js image optimization is failing under output: 'export', it's not a bug — the default loader needs a live server that a static export doesn't have. Set unoptimized: true if your images don't need per-request resizing, or configure a custom loader that delegates to whatever CDN already serves them, which is the pattern that keeps a fully static site shipping correctly sized images without needing a server anywhere in the picture.

If other server-dependent Next.js features are also breaking under the same static export, our dynamic server usage guide covers the broader unsupported-features list this one belongs to, and if a self-hosted Docker deployment is also serving stale cached pages alongside this, our ISR revalidation guide covers that separate static-export-adjacent constraint.

Pick the option that matches where your images actually live, and let next build finish the way it's supposed to — with images that load correctly, not a build that fails on the first <Image> component it finds.

Frequently Asked Questions

Because the default image loader relies on Next.js's built-in Image Optimization API, which resizes and re-encodes images on demand as users request specific sizes — a runtime operation that needs an actual server process handling requests. output: export produces a folder of static files with no server behind it at all, so the default loader has nothing to call, and Next.js fails the build rather than shipping a component that can't function in production.

It tells next/image to skip the Image Optimization API entirely and render a plain img tag pointing directly at the original image file, with none of the automatic resizing, format conversion, or lazy-loading srcset generation the optimizer normally provides. It's the simplest fix, and the right one when your images are already reasonably sized or served from something that optimizes them another way.

A custom loader is a function you provide that takes a source path, width, and quality, and returns a URL — typically pointing at a third-party image CDN like Cloudinary, Imgix, or in this project's case, Pexels's own URL-based resizing parameters. Because the loader just builds a URL string rather than calling Next.js's own server-side optimizer, it works identically whether the app is server-rendered or statically exported.

It depends on where your images actually come from. If they're already served by something with its own optimization and resizing — Pexels, Cloudinary, most modern CDNs — a custom loader gets you real width-specific, format-aware delivery for free, at the cost of one extra config file. If your images are local files with no external resizing service in front of them, unoptimized: true is simpler and honest about the tradeoff you're making rather than partially solving a problem the loader can't fully address without one.

Yes — instead of skipping optimization or delegating to a third-party CDN, this kind of tool runs an extra build step that pre-generates multiple resized versions of each local image as static files during next build, then uses a custom loader to serve the correctly sized version. It's a reasonable middle ground when your images are local and you don't have a CDN, at the cost of extra build time and disk space for the generated variants.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio