Skip to content

Platform-Specific Guides

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.

By · Editor

Last verified Jun 13, 2026

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 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.

Tip

Use BunnyCDN's $5 Free Credit

The quickest low-cost test is Bunny Optimizer behind next/image. Sign up here and apply coupon THEWPX for $5 of free credit while you test the loader.


Quick Decision

Use this as the starting point:

SituationBest FitWhy
Small Vercel site with modest image useDefault Vercel optimizerNo extra provider, no custom loader, works out of the box
Static-exported Next.js siteBunny, ImageKit, Cloudflare, or imgproxyThere is no Next.js server optimizer in a pure static export
Next.js deployed on NetlifyNetlify Image CDNNetlify's Next.js adapter wires image optimization into next/image
Lowest predictable optimizer costBunny Optimizer$9.50/month per website for Optimizer, bandwidth billed separately
Best developer URL APIImageKitClean transformations, free tier, storage, and broader media tooling
Already using CloudflareCloudflare ImagesGood fit with Cloudflare routing, Workers, R2, and transformation pricing
Rich media workflowCloudinarySmart crops, media management, background removal, and advanced transformations
Full controlimgproxy or ThumborYou 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:

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 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. The loader file path is relative to the project root, and the file exports a default function that returns a URL string.

// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.js',
    qualities: [50, 75, 90],
  },
}
// 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:

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.

ProviderCost ShapeBest ForMain Tradeoff
Bunny Optimizer$9.50/month per website plus CDN bandwidthLow-cost Next.js image deliveryLess polished transformation API
ImageKitFree tier, then Lite/Pro bandwidth and storage plansDeveloper-friendly transformations and media URLsPro costs more than basic CDN setups
Cloudflare Images5,000 free unique transformations, then $0.50/1,000Cloudflare-fronted sites and Workers/R2 usersMore Cloudflare-specific architecture
Netlify Image CDNIncluded through Netlify platform usageNext.js deployed on NetlifyBest when you stay on Netlify
CloudinaryCredit-based media platform pricingAdvanced media workflowsExpensive if you only need resizing
Vercel defaultVercel image usage metricsSmall or standard Vercel appsCosts can be harder to isolate at scale
imgproxy/ThumborYour infrastructure costFull control and custom policiesYou 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:

// 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:

// 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:

// 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 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:

// 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:

// 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:

// 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 and Thumbor 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:

// 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:

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:

// 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:

// 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.

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.

Free$5Credit
Live Offer

BunnyCDN · $0.01/GB

CodeTHEWPX
Claim