# Image CDN for Next.js: Loaders, Static Export & Cost Math (2026)

> A practical guide to using Bunny, ImageKit, Cloudflare, Netlify, Cloudinary, or a self-hosted image CDN with next/image in modern Next.js.

_Source: https://theimagecdn.com/docs/image-cdn-for-nextjs_

_Published: 2026-05-04 · Updated: 2026-06-13 · By Sunny Kumar_

Next.js gives you a good image component, but it does not make the image delivery decision for you forever. The default optimizer is convenient on Vercel, Netlify has its own platform image CDN, static export needs an external optimizer, and custom loaders let you send the same `<Image />` component through Bunny, ImageKit, Cloudflare, Cloudinary, or your own imgproxy service.

**TL;DR:** Keep the default Next.js image optimizer if your site is small, deployed on Vercel, and the included image usage is enough. Use [Bunny Optimizer](https://try.sunnysah.com/BunnyCDN) if you want the cheapest simple CDN-backed loader. Use ImageKit if developer experience and a clean transformation API matter. Use Cloudflare Images if your site already sits behind Cloudflare. Use Netlify Image CDN if you deploy Next.js to Netlify. Use Cloudinary only when you need its media workflow features, not because it is the cheapest way to resize images.

- [Decision](#quick-decision) — Which image CDN to use with Next.js based on deployment and cost.

- [Loaders](#how-nextimage-loaders-work) — The custom loader pattern and Next.js 16 notes.

- [Providers](#every-option-compared) — Bunny, ImageKit, Cloudflare, Netlify, Cloudinary, and imgproxy compared.

- [Static Export](#static-export-is-the-gotcha) — Why output: 'export' needs a custom loader or unoptimized images.

> **Use BunnyCDN's $5 Free Credit**
>
> The quickest low-cost test is Bunny Optimizer behind `next/image`. Sign up [here](https://try.sunnysah.com/BunnyCDN) and apply coupon **THEWPX** for $5 of free credit while you test the loader.

---

## Quick Decision

Use this as the starting point:

| Situation | Best Fit | Why |
| --- | --- | --- |
| Small Vercel site with modest image use | **Default Vercel optimizer** | No extra provider, no custom loader, works out of the box |
| Static-exported Next.js site | **Bunny, ImageKit, Cloudflare, or imgproxy** | There is no Next.js server optimizer in a pure static export |
| Next.js deployed on Netlify | **Netlify Image CDN** | Netlify's Next.js adapter wires image optimization into `next/image` |
| Lowest predictable optimizer cost | **Bunny Optimizer** | $9.50/month per website for Optimizer, bandwidth billed separately |
| Best developer URL API | **ImageKit** | Clean transformations, free tier, storage, and broader media tooling |
| Already using Cloudflare | **Cloudflare Images** | Good fit with Cloudflare routing, Workers, R2, and transformation pricing |
| Rich media workflow | **Cloudinary** | Smart crops, media management, background removal, and advanced transformations |
| Full control | **imgproxy or Thumbor** | You own the service, cache, security, and cost model |

The most common mistake is comparing all of these as if they are the same thing. They are not.

Vercel's optimizer is a hosting-platform feature. Netlify Image CDN is a hosting-platform feature. Bunny Optimizer is a CDN add-on. ImageKit and Cloudinary are media platforms. Cloudflare Images is a Cloudflare product that can either transform remote images or host images. imgproxy is software you run.

The `next/image` API lets all of them fit behind one component, but the operational model is different.

---

## What does "Image CDN for Next.js" actually mean?

In a Next.js project, "image CDN" can mean one of three things.

First, it can mean the default optimizer behind `next/image`. You write:

```jsx
import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/images/hero.jpg"
      width={1200}
      height={630}
      alt="Product dashboard"
    />
  )
}
```

The [Next.js Image component](https://nextjs.org/docs/app/api-reference/components/image) generates responsive `srcset` URLs, lazy-loads by default, helps avoid layout shift, and sends image requests to the configured optimizer. On Vercel, that optimizer is handled by Vercel. On Netlify, the adapter can route `next/image` through Netlify Image CDN.

Second, it can mean a third-party optimizer behind a custom loader. Your components stay mostly the same, but the loader returns a URL for Bunny, ImageKit, Cloudflare, Cloudinary, or another service.

Third, it can mean bypassing Next.js image optimization completely and using a CDN URL directly in an `<img>` tag. That is sometimes right for simple markdown content, but it gives up useful `next/image` behavior unless you recreate it.

This guide is about the second case: keeping `next/image` while moving optimization and delivery to a dedicated image CDN.

---

## How do Next.js image loaders work?

A custom loader is a small function. It receives `src`, `width`, and `quality`, then returns the final image URL.

In modern Next.js, a global loader is configured with `images.loader = 'custom'` and [`images.loaderFile`](https://nextjs.org/docs/app/api-reference/config/next-config-js/images#loaderfile). The loader file path is relative to the project root, and the file exports a default function that returns a URL string.

```js
// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.js',
    qualities: [50, 75, 90],
  },
}
```

```js
// lib/image-loader.js
'use client'

export default function imageLoader({ src, width, quality }) {
  return `https://cdn.example.com${src}?width=${width}&quality=${quality || 75}`
}
```

That is the core integration.

You can also use a per-component loader when only some images need a different provider:

```jsx
import Image from 'next/image'

function editorialLoader({ src, width, quality }) {
  return `https://res.cloudinary.com/demo/image/fetch/f_auto,q_${quality || 'auto'},w_${width}/${src}`
}

export default function ArticleImage() {
  return (
    <Image
      loader={editorialLoader}
      src="https://example.com/photo.jpg"
      width={1200}
      height={800}
      alt="Editorial photograph"
    />
  )
}
```

Two Next.js 16 details matter.

First, `priority` has been deprecated in favor of `preload`. For a hero image that is likely to be the LCP element, use `preload`, `loading="eager"`, or `fetchPriority="high"` intentionally. Do not preload several competing hero images.

Second, `qualities` is now part of cost control. If you let every random quality value create a unique derivative, a malicious or sloppy request pattern can inflate transformation volume. Keep a short allowlist such as `[50, 75, 90]` unless you have a reason to support more.

---

## How has Vercel cost math changed?

Older Next.js image cost guides often talk about Vercel billing by "source images." That was the old mental model and it still appears in stale posts.

Current Vercel image optimization pricing is based on three usage metrics:

- **Image transformations:** charged when an optimized image is generated on cache MISS or STALE.
- **Image cache reads:** read units used to access cached optimized images from the global cache.
- **Image cache writes:** write units used to store optimized images in the global cache.

Vercel's current docs list Hobby included usage at 5,000 image transformations per month, 300,000 image cache read units per month, and 100,000 image cache write units per month. On-demand rates are regional and include transformation, cache-read, and cache-write charges. Vercel also notes that Fast Data Transfer and Edge Requests can apply when transformed images are delivered from its CDN.

That pricing is more usage-aligned than the old source-image model. It also means the cost conversation has changed.

The question is no longer only:

"How many original images do I have?"

The better questions are:

- How many unique image derivatives are generated each month?
- How often do stale images need to be regenerated?
- How large are the optimized responses?
- How often are cached images read from global cache?
- Are images part of a commercial site that cannot use Hobby?
- Would an external image CDN give clearer cost boundaries?

For many Vercel apps, the default optimizer is still fine. It is integrated, supported, and avoids another vendor. You should move away when image costs become a visible line item, when static export removes the optimizer, or when you need transformation features Vercel does not provide.

---

## How do Next.js image CDNs compare?

Use this table as a shortlist, not as a universal ranking.

| Provider | Cost Shape | Best For | Main Tradeoff |
| --- | --- | --- | --- |
| **Bunny Optimizer** | $9.50/month per website plus CDN bandwidth | Low-cost Next.js image delivery | Less polished transformation API |
| **ImageKit** | Free tier, then Lite/Pro bandwidth and storage plans | Developer-friendly transformations and media URLs | Pro costs more than basic CDN setups |
| **Cloudflare Images** | 5,000 free unique transformations, then $0.50/1,000 | Cloudflare-fronted sites and Workers/R2 users | More Cloudflare-specific architecture |
| **Netlify Image CDN** | Included through Netlify platform usage | Next.js deployed on Netlify | Best when you stay on Netlify |
| **Cloudinary** | Credit-based media platform pricing | Advanced media workflows | Expensive if you only need resizing |
| **Vercel default** | Vercel image usage metrics | Small or standard Vercel apps | Costs can be harder to isolate at scale |
| **imgproxy/Thumbor** | Your infrastructure cost | Full control and custom policies | You own operations |

The best cheap answer is usually Bunny. The best free-for-now answer is usually ImageKit or Cloudflare, depending on traffic and architecture. The best zero-new-account answer is your hosting platform's default image optimization. The best media-platform answer is Cloudinary.

---

## 1. Bunny Optimizer: The Cheapest Simple Loader

Bunny Optimizer is the easiest recommendation when the goal is low-cost image optimization for a Next.js site.

Bunny's current Optimizer documentation lists the price at $9.50 per website per month, including unlimited optimizations, requests, and transformations. CDN bandwidth is billed separately. That pricing shape is simple: pay a small fixed fee for the optimizer, then pay CDN transfer.

The loader is straightforward:

```js
// lib/bunny-loader.js
'use client'

export default function bunnyLoader({ src, width, quality }) {
  const params = new URLSearchParams({
    width: String(width),
    quality: String(quality || 75),
  })

  return `https://your-zone.b-cdn.net${src}?${params.toString()}`
}
```

Wire it globally:

```js
// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/bunny-loader.js',
    qualities: [50, 75, 90],
  },
}
```

Setup is usually:

1. Create a Bunny pull zone pointed at your origin.
2. Enable Optimizer on that pull zone.
3. Add the loader file.
4. Configure `loaderFile`.
5. Deploy and inspect Network requests.

The thing to verify is that images are actually transformed. If Optimizer is not enabled, query parameters may not do what you expect. You can accidentally serve full-size originals through a CDN and think image optimization is working because the hostname changed.

Use Bunny when you want cheap, boring, scalable image delivery and do not need a full media platform.

---

## 2. ImageKit: The Best Developer Experience

ImageKit is the best fit when the URL transformation API matters. The syntax is readable, the docs are built for developers, and the product includes more than just resize-and-compress.

Current ImageKit pricing has a free tier with 20 GB bandwidth and 3 GB DAM storage. The Lite plan starts at $9/month with 40 GB bandwidth and pay-as-you-go overage. Pro is $89/month with 225 GB bandwidth and 225 GB DAM storage.

The loader is clean:

```js
// lib/imagekit-loader.js
'use client'

export default function imagekitLoader({ src, width, quality }) {
  const transformations = [`w-${width}`, `q-${quality || 75}`, 'f-auto']
  return `https://ik.imagekit.io/your_id${src}?tr=${transformations.join(',')}`
}
```

ImageKit is especially good for:

- Product images.
- Marketing sites with many responsive layouts.
- Teams that need predictable transformation URLs.
- Apps that may need storage, media management, or URL endpoints later.
- Sites that fit inside the free or Lite bandwidth levels.

It is not always the cheapest at high delivery volume. Bunny plus CDN bandwidth can be cheaper for simple resizing. Cloudflare can be cheaper when the number of unique transformations is low. But ImageKit is often the smoothest middle ground for teams that want a real image platform without Cloudinary-level complexity.

See the [ImageKit free plan limits guide](https://theimagecdn.com/docs/free-image-cdns/imagekit-free-plan-limits) if you are considering starting on the free tier.

---

## 3. Cloudflare Images: Best If Your Site Already Uses Cloudflare

Cloudflare Images is the strongest fit when your DNS, Workers, Pages, R2, or caching strategy already lives in Cloudflare.

Cloudflare's current Images pricing has a free transformations plan: 5,000 unique transformations per month. After that, new transformations on the Free plan return a `9422` error rather than silently billing you. On the Paid plan, transformations include the first 5,000 unique transformations and then cost $0.50 per 1,000 unique transformations per month. Hosted Cloudflare Images storage and delivery use separate storage and delivery metrics.

For remote image transformations, the loader can use Cloudflare's `/cdn-cgi/image/` URL format:

```js
// lib/cloudflare-loader.js
'use client'

export default function cloudflareLoader({ src, width, quality }) {
  const options = [`width=${width}`, `quality=${quality || 75}`, 'format=auto']
  return `https://www.example.com/cdn-cgi/image/${options.join(',')}${src}`
}
```

This assumes your site is routed through Cloudflare and the source path resolves correctly.

Cloudflare is attractive because unique transformations are counted monthly, not every request. If `/hero.jpg` at `width=1200,format=auto` is requested many times in the same month, it is still one unique transformation for that parameter set. But every distinct width, crop, format option, or source image can create another unique transformation.

For cost control:

- Keep `deviceSizes` reasonable.
- Keep `qualities` short.
- Do not generate arbitrary widths from user input.
- Avoid dozens of near-identical breakpoints.
- Use `format=auto` instead of separate AVIF and WebP URLs when possible.

Use Cloudflare when Cloudflare is already the front door. Do not adopt the whole Cloudflare stack only because you need a `next/image` loader.

---

## 4. Netlify Image CDN: Best If You Deploy Next.js On Netlify

If your Next.js app deploys to Netlify, start with Netlify Image CDN. Netlify's Next.js documentation says `next/image` uses Netlify Image CDN by default, and the Next.js adapter enables image optimization for supported projects.

That makes Netlify different from Bunny or ImageKit. You may not need a custom loader at all. Deploy the app to Netlify, configure remote image allowlists correctly, and let the platform adapter handle the route.

Netlify also exposes an image CDN endpoint directly:

```js
// lib/netlify-loader.js
'use client'

export default function netlifyLoader({ src, width, quality }) {
  const params = new URLSearchParams({
    url: src,
    w: String(width),
    q: String(quality || 75),
  })

  return `/.netlify/images?${params.toString()}`
}
```

Use the direct endpoint when you have a specific reason. For normal Netlify-hosted Next.js apps, the platform integration is the cleaner path.

Netlify is best when:

- You already deploy Next.js on Netlify.
- You want one hosting bill.
- You do not need a provider-independent image pipeline.
- Your image traffic fits the Netlify plan.

It is weaker when you want your image delivery to be portable across hosts. If you may move from Netlify to Vercel, Cloudflare Pages, or static hosting later, an independent image CDN is easier to carry with you.

---

## 5. Cloudinary: Best For Advanced Media Workflows

Cloudinary is overkill for a simple blog that only needs resized WebP images. It is excellent for media-heavy products.

Cloudinary's free plan includes 25 monthly credits. Credits are shared across transformations, storage, and bandwidth. One credit can equal 1,000 transformations, 1 GB of storage, or 1 GB of image bandwidth for standard image usage. Paid self-service plans and enterprise plans add more credits and features.

A simple loader looks like this:

```js
// lib/cloudinary-loader.js
'use client'

export default function cloudinaryLoader({ src, width, quality }) {
  const encodedSrc = encodeURIComponent(src)
  const transforms = `f_auto,q_${quality || 'auto'},w_${width}`
  return `https://res.cloudinary.com/your-cloud/image/fetch/${transforms}/${encodedSrc}`
}
```

Cloudinary makes sense when you need:

- Face-aware crops.
- Background removal.
- Generative or AI-assisted media features.
- Asset management.
- Editorial workflows.
- Video plus image infrastructure.
- Advanced transformation chains.

It is not the cheapest way to use `next/image`. If your whole requirement is "resize to 640, 1080, and 1600 pixels," use Bunny, Cloudflare, ImageKit, Netlify, or Vercel first.

---

## 6. Self-Hosted Imgproxy: When You Need Control

[imgproxy](https://imgproxy.net/) and [Thumbor](https://thumbor.readthedocs.io/) are open-source image transformation services you can run yourself. The Next.js loader pattern is the same. Your loader returns URLs to your own optimizer service.

Example shape:

```js
// lib/imgproxy-loader.js
'use client'

export default function imgproxyLoader({ src, width, quality }) {
  const encodedSrc = btoa(src)
  return `https://images.example.com/insecure/resize:fit:${width}/quality:${quality || 75}/plain/${encodedSrc}`
}
```

Do not copy that exact code into production without reading your chosen service's URL signing and encoding rules. A real imgproxy setup should use signed URLs, a CDN in front, origin restrictions, memory limits, and a clear cache policy.

Self-hosting makes sense when:

- You have strict compliance requirements.
- You need custom transformation policy.
- You already operate infrastructure.
- Your volume is high enough to justify ownership.
- Vendor lock-in is more expensive than operational work.

It does not make sense because "a VPS is $5/month." The server is cheap. The maintenance is not. You own security patches, image library vulnerabilities, cache invalidation, scaling, monitoring, abuse prevention, and incident response.

For most teams, Bunny's $9.50/month Optimizer fee is cheaper than one hour of maintenance.

---

## Why is Next.js static export a gotcha?

Static export changes the whole decision.

If your project uses:

```js
module.exports = {
  output: 'export',
}
```

then you do not have a Next.js server optimizer at runtime. A static export is HTML, CSS, JS, and assets. There is no server function waiting to resize `/images/photo.jpg` on demand.

That leaves two paths:

1. Use `images: { unoptimized: true }` and serve original image files.
2. Use a custom loader that points at an external image CDN.

The first option is fine for tiny sites with already-optimized assets. It is bad for image-heavy pages because it removes the core benefit of `next/image` optimization.

The second option is usually the right answer:

```js
// next.config.js
module.exports = {
  output: 'export',
  images: {
    loader: 'custom',
    loaderFile: './lib/bunny-loader.js',
    qualities: [50, 75, 90],
  },
}
```

Now the static site can still render responsive `srcset` values, but the actual resizing and format conversion happens on Bunny, ImageKit, Cloudflare, or another image CDN.

This is the setup many static Next.js guides skip. They explain `next/image`, then assume Vercel's server optimizer exists. That assumption is false for static export.

---

## How do you handle remote images and security?

If you use remote images with `next/image`, configure `remotePatterns`. Do not rely on the older `images.domains` style unless you are maintaining older code. Next.js has deprecated `domains` in favor of stricter `remotePatterns`.

Example:

```js
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        pathname: '/account123/**',
      },
    ],
    qualities: [50, 75, 90],
  },
}
```

Why this matters:

- A loose remote allowlist can let attackers use your optimizer as an open proxy.
- Arbitrary query strings can produce endless unique derivatives.
- SVG handling can create security problems if you enable it casually.
- Redirects from allowed hosts need attention.
- Huge source images can stress memory and cache storage.

An image CDN does not remove these concerns. It moves them to a different layer. Keep your source domains strict and your transformation options bounded.

---

## How do you migrate from Vercel Default to BunnyCDN?

Use this process for Bunny, ImageKit, or Cloudflare. The exact loader changes, but the migration shape is the same.

### 1. Inventory Image Sources

Find where images come from:

- `/public` assets.
- CMS remote URLs.
- User-uploaded media.
- Markdown content.
- Open Graph images.
- Product images.
- Avatar providers.

Do not switch loaders until you know whether the CDN can fetch every source image.

### 2. Create The CDN Origin

For Bunny, create a pull zone pointed at your site or storage origin. Enable Optimizer. For Cloudflare, confirm your route and transformation setup. For ImageKit, configure URL endpoints or upload storage.

### 3. Add The Loader

Start with one loader file and a narrow `qualities` list.

```js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/bunny-loader.js',
    qualities: [50, 75, 90],
  },
}
```

### 4. Check Rendered HTML

Open the browser, inspect images, and verify:

- The `srcset` points to the CDN.
- Width parameters are present.
- Quality parameters are present.
- The response `Content-Type` is WebP, AVIF, JPEG, or PNG as expected.
- The largest image is not accidentally loading at original size.

### 5. Measure LCP

Do not assume the CDN helped. Measure.

Use Lighthouse, WebPageTest, Chrome DevTools, or real user monitoring. Compare:

- LCP image URL.
- LCP timing.
- Transfer size.
- Cache status.
- First request after deploy.
- Repeat request after cache warmup.

The first uncached transformation can be slower than the old path. The repeat cached request is what should improve.

### 6. Roll Out Carefully

For large sites, migrate a template or route group first. Watch 404s and image response sizes. If the CDN URL builder is wrong, every image can break at once.

---

## Common Mistakes

**Using a custom loader but forgetting the provider feature.** A Bunny pull zone without Optimizer is not an image optimizer. It is just a CDN.

**Letting arbitrary quality values through.** Keep a short `qualities` list. It protects cost and cache efficiency.

**Creating too many widths.** Every unique width can create another derivative. Use sane `deviceSizes` and `imageSizes`.

**Forgetting `sizes` on responsive images.** Without `sizes`, browsers may choose less efficient candidates. For full-width images, use something like `sizes="100vw"`. For grid cards, describe the actual layout.

**Using `preload` everywhere.** Preload the one likely LCP image, not every image above the fold.

**Optimizing SVGs through raster pipelines.** SVG is vector. Serve it carefully with proper security headers or as a normal asset.

**Not checking cache headers.** If your origin sends poor cache headers, your image CDN may revalidate too often.

**Assuming static export behaves like Vercel.** It does not. Use `unoptimized` or a custom external loader.

---

## Frequently asked questions

### Does next/image work with a custom image CDN?

Yes. Next.js supports a global `loaderFile` and per-component `loader` prop. The loader receives `src`, `width`, and `quality`, then returns the URL that the browser should fetch. This lets you keep the `<Image />` component while moving optimization to Bunny, ImageKit, Cloudflare, Cloudinary, imgproxy, or another service.

### Is Vercel's default image optimizer still billed by source images?

For current default pricing, Vercel documents image optimization billing around image transformations, image cache reads, and image cache writes. Some legacy Enterprise contexts may still have older source-image pricing, but new cost planning should use the current transformation and cache-unit model.

### What is the cheapest image CDN for Next.js?

For a practical paid setup, Bunny Optimizer is usually the cheapest simple answer because Optimizer is $9.50/month per website and transformations are unlimited, with CDN bandwidth billed separately. Cloudflare can be cheaper for low unique-transformation volume. ImageKit can be free if your traffic fits its free tier.

### Does Next.js static export support image optimization?

Not through the default server optimizer. With `output: 'export'`, you either set `images.unoptimized = true` and serve images as-is, or you use a custom loader that points to an external image CDN. For image-heavy static sites, the custom loader path is usually better.

### Should I use Netlify Image CDN with Next.js?

Yes, if your Next.js site is deployed on Netlify and you are happy staying there. Netlify's Next.js adapter supports image optimization with Netlify Image CDN. If you want a host-independent image pipeline, use an external CDN such as Bunny, ImageKit, Cloudflare, or Cloudinary instead.

### Can I use multiple image CDNs in one Next.js app?

Yes. Use a global loader for the default path and pass a different `loader` prop to specific `<Image />` components when needed. For example, static marketing images can use Bunny while editorial assets that need smart crops use Cloudinary.

### Will a custom image CDN improve Core Web Vitals?

It can improve LCP when it reduces transfer size, improves cache hit rate, or serves from a faster edge path. It can also do nothing if your old setup was already cached well or if the new loader creates poor sizes. Always verify the LCP image URL, transfer size, content type, and cache status after migration.

## Bottom Line

The right image CDN for Next.js depends less on React and more on deployment, cost shape, and media workflow.

For a small Vercel app, keep the default optimizer until it becomes a real cost or feature problem. For a static export, use a custom loader because there is no runtime optimizer. For Netlify, start with Netlify Image CDN. For cheap independent delivery, use Bunny Optimizer. For a cleaner developer platform, use ImageKit. For Cloudflare-heavy stacks, use Cloudflare Images. For advanced media operations, use Cloudinary. For total control, run imgproxy and accept the operational work.

The best part is that `next/image` keeps the switching cost low. Put the provider-specific logic in one loader file, keep your component API stable, and make image delivery a replaceable infrastructure decision instead of a rewrite.
