Next.js optimizes images on demand through the /_next/image endpoint. It resizes them, converts them to WebP or AVIF, and improves Largest Contentful Paint and Cumulative Layout Shift. Vercel bills per source image. That can turn into a large monthly overage on catalog-scale sites. Third-party loaders, self-hosting with sharp, and build-time optimization all avoid that specific cost.

Key takeaways

  • next/image resizes, reformats, and caches images through the /_next/image endpoint. It improves LCP and CLS with little manual work.
  • Vercel bills per source image once you exceed plan limits. Catalog-scale sites can see real monthly overage (check Vercel's pricing).
  • Third-party loaders such as Cloudinary skip Vercel's optimization endpoint entirely. You trade it for a separate transformation quota and bill.
  • Self-hosting with sharp removes per-image charges. But it shifts cost to server CPU time and cache-warming complexity.
  • Build-time pre-optimization gives steady costs for static content. It breaks down for user-uploaded or CMS-driven images.

Updated 15 August 2026: sources added, experience claims checked against our project record, summary added.

How Does next/image Actually Work?

When a browser asks for an image through next/image, the server (or edge function) resizes the original to the width it needs. It then converts the image to a modern format like WebP or AVIF, if the browser supports it, and caches the result. Later requests for the same width and format hit the cache.

Here's the actual flow:

  1. Request parsing. The /_next/image endpoint gets a request with query parameters: url (the source image), w (the requested width), and q (quality, defaulting to 75).
  2. Source fetching. The optimization API fetches the original image, either from the local filesystem or a remote URL you've allowed in next.config.js under images.remotePatterns.
  3. Resize and encode. On Vercel, this uses their own optimization system. Self-hosted, Next.js uses sharp (a Node.js binding for libvips) to resize and encode.
  4. Format negotiation. The server reads the Accept header. If the browser sends image/avif, you get AVIF. If it sends image/webp, you get WebP. Otherwise, you get the original format.
  5. Cache. The optimized image is stored with a Cache-Control header. On Vercel, this lives in their edge cache. Self-hosted, it lands in .next/cache/images/.
import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Product hero shot"
      width={1200}
      height={630}
      priority
      sizes="100vw"
    />
  )
}

This works great for a 20-page marketing site. Things get harder when you have thousands of product images, user-uploaded content, or a CMS feeding in images at random sizes.

What Is the Image Optimization API?

The Image Optimization API is the server-side endpoint (/_next/image) that Next.js uses to resize, reformat, and serve images. It does not run at build time. Images are processed on the first request and cached after that.

Configuration lives in next.config.js:

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60, // seconds
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      },
    ],
  },
}
  • formats controls which modern formats Next.js will serve. AVIF comes first, WebP is the fallback. AVIF encodes slower but makes smaller files than WebP at similar visual quality.
  • deviceSizes and imageSizes set the widths the API will generate. The srcset attribute on the rendered <img> element draws from these arrays.
  • minimumCacheTTL sets how long optimized images stay cached. The default is 60 seconds, which is too low for most production sites. Teams usually raise this to at least 2592000 seconds (30 days) so images don't get reoptimized needlessly.

The sizes attribute is one of the most often skipped parts. Without it, the browser downloads images far larger than the screen needs. That hurts mobile performance.

How next/image Improves Core Web Vitals

next/image directly affects all three Core Web Vitals. Its biggest wins are on Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS).

Largest Contentful Paint (LCP)

The LCP element on most pages is an image. next/image improves LCP three ways:

  1. Smaller file sizes. Serving WebP/AVIF at the right width means fewer bytes to download. A 3840px-wide JPEG at 200KB becomes a 750px-wide WebP at 35KB on mobile.
  2. priority prop. Adding priority to your above-the-fold image turns off lazy loading, adds fetchpriority="high", and preloads the image in <head>. This can noticeably improve LCP on image-heavy pages, since the browser starts fetching the image right away instead of finding it after layout.
  3. Format negotiation. AVIF and WebP decode faster than JPEG in most browsers, cutting paint time.

Cumulative Layout Shift (CLS)

Because next/image needs width and height props (or fill), it saves the right space in the layout before the image loads. This stops the page from jumping. This alone often prevents a good chunk of layout shift that would otherwise count against CLS.

Interaction to Next Paint (INP)

Smaller images mean less main-thread work for decoding. On low-end Android phones, a large, unoptimized hero image can block the main thread for a noticeable stretch while decoding. Serving a properly sized, compressed WebP mostly removes that delay.

Our SleepDr.com migration paired this kind of image optimization with a broader Next.js 15 rebuild and took its Lighthouse performance score from 35 to 94. More on our approach to Core Web Vitals is at /capabilities/core-web-vitals-optimization/.

The Vercel Cost Trap: Real Numbers

Vercel's pricing for image optimization is per source image, not per request. As of 2026, Vercel's published pricing breaks down like this:

Plan Included Source Images Overage Cost
Hobby (free) 1,000 / month N/A (hard limit)
Pro ($20/mo/member) 5,000 / month $5 per 1,000 images
Enterprise Custom Custom

A "source image" means each unique original that gets optimized. If you run a product catalog with 3,000 SKUs, each with 3 images, that's 9,000 source images. On the Pro plan, you'd blow past the 5,000 included images and pay $20 in overage every month. That happens even with a warm cache, since the counter resets monthly.

Take an e-commerce site with 15,000 product images. Its monthly Vercel image optimization cost would look like this:

15,000 - 5,000 included = 10,000 overage images
10,000 / 1,000 × $5 = $50/month in image optimization alone

That's $600 a year just for image resizing. Not hosting, not serverless functions, not bandwidth.

The surprise factor is real. Teams often spot these charges only after the first invoice, since Vercel's dashboard shows usage without calling out this cost clearly during development.

Alternative 1: Third-Party Loaders (Cloudinary, imgix)

The most common workaround is a custom image loader. Next.js supports this out of the box. You swap out the optimization backend while keeping the <Image> component's lazy loading and layout shift prevention.

How Custom Loaders Work

A loader is a function that takes src, width, and quality and returns a URL. That URL points to your third-party service, which handles the resize and format conversion.

// lib/cloudinary-loader.ts
export default function cloudinaryLoader({
  src,
  width,
  quality,
}: {
  src: string
  width: number
  quality?: number
}) {
  const params = [
    'f_auto',
    'c_limit',
    `w_${width}`,
    `q_${quality || 'auto'}`,
  ]
  return `https://res.cloudinary.com/your-cloud/image/upload/${params.join(',')}${src}`
}

Then in next.config.js:

module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/cloudinary-loader.ts',
  },
}

With this setup, Vercel's Image Optimization API is never called. Zero source images counted. Your Cloudinary (or imgix, or ImageKit) plan handles everything.

Cost Comparison

Cloudinary's free tier includes 25,000 transformations a month, and its Plus plan at $89/month includes 225,000 transformations. imgix prices its paid plans by origin images with unlimited transformations included. For a 15,000-image catalog:

Service Monthly Cost Notes
Vercel built-in ~$70 ($20 Pro + $50 overage) Per source image reset monthly
Cloudinary Plus $89 225K transformations, generous
imgix (paid tier) Custom, origin-based Unlimited transforms
Self-hosted sharp $0-20 (server cost) You maintain it

If you're weighing Cloudinary against other image services, we wrote a detailed comparison at /blog/cloudinary-alternatives-2026/.

Tradeoffs

Pros: No Vercel optimization charges. Better transformation tools, like smart cropping, face detection, and overlays. Global CDNs built for images.

Cons: Another vendor to manage. Another bill. Origin images need to live in or be reachable by the service. Possible mismatched cache invalidation between your CDN and the image service.

Alternative 2: Self-Hosting with sharp

If you self-host Next.js (on a VPS, Docker container, or Kubernetes), the Image Optimization API uses sharp by default. There are no per-image charges. You pay only for compute time and storage.

Setup

sharp installs automatically when you run next build on a self-hosted deployment. You can check by looking in your node_modules:

npm ls sharp
## should show sharp@0.33.x

For Docker deployments, make sure your base image includes the native dependencies sharp needs:

FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat

## sharp will compile native bindings during npm install

Cache Configuration

The cached images live in .next/cache/images/. On a containerized deployment, you need to keep this folder across deploys or you'll re-optimize every image on every deployment.

// next.config.js
module.exports = {
  images: {
    minimumCacheTTL: 2592000, // 30 days
  },
}

Mount .next/cache as a lasting volume. On Railway or Render, this is simple. On Kubernetes, use a PVC.

Performance Considerations

sharp resizes and encodes images fast enough for on-demand serving under normal load. But if many unique images get requested at once on a cold cache, each request becomes its own sharp job competing for CPU. That competition can noticeably slow response times while the cache warms up.

Mitigation strategies:

  1. Pre-warm the cache by crawling your site after deployment.
  2. Rate-limit concurrent optimizations (Next.js doesn't do this on its own, but you can add a queue in a custom server).
  3. Use a CDN in front (Cloudflare, Fastly) so the cache-warming rush only hits your origin once per edge location.

Alternative 3: Pre-Optimizing at Build Time

The most predictable approach: optimize all images before they reach the user, during your build or CI pipeline.

Using sharp in a Build Script

// scripts/optimize-images.mjs
import sharp from 'sharp'
import { glob } from 'glob'
import path from 'path'

const images = await glob('public/images/**/*.{jpg,png}')

for (const img of images) {
  const outputWebP = img.replace(/\.(jpg|png)$/, '.webp')
  const outputAVIF = img.replace(/\.(jpg|png)$/, '.avif')

  await sharp(img)
    .resize(1200, null, { withoutEnlargement: true })
    .webp({ quality: 80 })
    .toFile(outputWebP)

  await sharp(img)
    .resize(1200, null, { withoutEnlargement: true })
    .avif({ quality: 65 })
    .toFile(outputAVIF)
}

Then use a plain <img> tag with a <picture> element, or set unoptimized: true on next/image and point to the pre-optimized files.

<Image
  src="/images/hero.webp"
  alt="Hero"
  width={1200}
  height={630}
  unoptimized
  priority
/>

When This Makes Sense

  • Static sites or mostly-static content
  • Images that don't change between deployments
  • When you want zero runtime image processing cost

When It Doesn't

  • User-uploaded images
  • CMS-driven content where editors upload images of any size
  • Sites with thousands of images. Build times grow fast. 10,000 images at 2 formats each means 20,000 sharp jobs, roughly 15-20 minutes on a 4-vCPU CI runner.

If you're working with Astro instead of Next.js, build-time optimization is the default there. We covered the differences in /blog/astro-image-optimization-2026/.

Priority, Sizes, and Placeholder: Practical LCP Guidance

These three props are where most teams leave LCP points on the table.

The `priority` Prop

Add priority to exactly one image per page, the LCP image. This does three things:

  1. Turns off lazy loading
  2. Sets fetchpriority="high" on the <img> element
  3. Adds a <link rel="preload"> to the document <head>
<Image src="/hero.jpg" alt="Hero" width={1200} height={630} priority />

Do not add priority to more than one or two images. Preloading everything is the same as preloading nothing. The browser can't prioritize when everything is marked high-priority.

The `sizes` Prop

This is the most often missed prop. Without it, the browser assumes the image is 100vw wide, so it downloads the largest version even on a 375px-wide phone.

Here's a typical layout pattern:

// Full-width hero
<Image sizes="100vw" ... />

// Two-column grid on desktop, full-width on mobile
<Image sizes="(min-width: 768px) 50vw, 100vw" ... />

// Sidebar thumbnail
<Image sizes="(min-width: 1024px) 256px, (min-width: 768px) 33vw, 50vw" ... />

Getting sizes right can cut image payloads a lot on mobile, since phones stop downloading desktop-sized files they'll never show at full size.

The `placeholder` Prop

For perceived speed, use placeholder="blur" with a blurDataURL. For local images imported as modules, Next.js builds the blur hash for you:

import heroImg from '@/public/hero.jpg'

<Image src={heroImg} alt="Hero" placeholder="blur" priority />

For remote images, you need to build the blurDataURL yourself. A common way is using plaiceholder (version 3.x) in the data-fetching layer:

import { getPlaiceholder } from 'plaiceholder'

const { base64 } = await getPlaiceholder(imageBuffer)
// Pass base64 as blurDataURL

The blur placeholder doesn't change LCP directly. The browser doesn't count the placeholder as the LCP element. But it makes load time feel much faster. Users see a blurred preview instead of a blank space.

Comparison Table: Optimization Approaches

Approach Runtime Cost Build Cost Best For LCP Impact Complexity
Vercel built-in $5/1K images over 5K None Small sites (<5K images) Excellent with priority Low
Cloudinary loader $0-89/mo None Large catalogs, CMS-driven Excellent Medium
imgix loader Custom, origin-based None High-traffic media sites Excellent Medium
Self-hosted sharp Server CPU cost None Cost-sensitive, self-hosted Excellent Medium-High
Build-time pre-optimization None CI minutes Static/semi-static sites Good (no responsive srcset) Medium
Hybrid (build + loader) Varies Some CI Mixed content sites Excellent High

The Supabase Storage Angle

Supabase Storage supports on-the-fly resizing through URL parameters. This is worth a look for teams already using Supabase as their backend. You can build a custom Next.js loader that points to Supabase's transformation URLs. That said, their image transformation is still fairly young next to Cloudinary or imgix. Format support is narrower, and CDN coverage is less mature. This fits prototypes and small apps well. Production e-commerce or media-heavy sites are usually better served by a dedicated image service.

FAQ

Does next/image work with static exports?

No. Static exports (output: 'export' in next.config.js) do not support the Image Optimization API because there's no server to handle on-demand resizing. Use unoptimized: true and pre-optimize images at build time, or use a third-party loader.

How much does Vercel image optimization cost?

Vercel's Pro plan includes 5,000 optimized source images per month, with each extra 1,000 images billed at $5. A site with 15,000 product images would pay roughly $50 per month in overage on top of the $20-per-member Pro plan fee, based on Vercel's published pricing.

Can I use AVIF with next/image?

Yes. Add 'image/avif' to the images.formats array in next.config.js, and Next.js serves AVIF to any browser that sends image/avif in its Accept header. Browser support is now wide across modern browsers, including Chrome, Firefox, and Safari, so AVIF works for most visitors.

What's the difference between sizes and deviceSizes? deviceSizes in next.config.js sets which widths the optimization API builds ahead of time. sizes on the <Image> component tells the browser which of those widths to download based on the current viewport. They work together: one sets the available widths, the other tells the browser which one fits the current layout.

Should I use fill or explicit width/height? Use explicit width and height when you know the image dimensions at build time. Use fill when the image should stretch to fit a parent container (like a card thumbnail). With fill, you must set sizes or the browser will download the largest version.

How do I prevent Vercel image optimization charges entirely?

Set a custom loaderFile in next.config.js pointing to a third-party service like Cloudinary or imgix. This skips Vercel's /_next/image endpoint completely, so no source images are counted, and Vercel's usage dashboard drops to zero because the built-in Image Optimization API is never called.

Does self-hosting Next.js with sharp have any limitations?

The main limits are cold-cache delay and CPU competition during cache warming, especially under heavy traffic to many unique images at once. sharp 0.33.x handles most common formats well, though AVIF encoding takes noticeably longer to compute than WebP. Plan for a cache pre-warming step after each deploy to avoid slow first loads.

How does next/image compare to Astro's image optimization?

Astro optimizes images at build time by default, while Next.js does it on demand at runtime. Astro's approach means zero runtime cost but longer builds, since every image version gets made before deployment instead of on first request. We compared the two approaches, including trade-offs for CMS-driven content, in detail at /blog/astro-image-optimization-2026/.