IAnswerable

Next.js Image Optimization When Self-Hosting

08 Jan 2026

next/image on Vercel offloads resizing to their image CDN. Self-hosted, that same work runs on your server, using your CPU, on every unique image/size/format combination the first time it’s requested. Get this wrong and a traffic spike turns into a CPU spike.

Why it works differently off Vercel

next/image doesn’t just lazy-load — it resizes, converts to WebP/AVIF, and caches the result. On Vercel that pipeline runs on their infrastructure. Self-hosted, Next.js does it in-process using sharp, and the resized output is cached to disk (.next/cache/images by default).

The first request for a given image at a given size is expensive. Every request after that, for the same size, is a cache hit — until your cache gets evicted or the container restarts and wipes it.

Install sharp — don’t skip it

Without sharp installed, Next.js falls back to the slower, unmaintained squoosh WASM path, or in some versions just warns and serves unoptimized images. Install it explicitly:

npm install sharp

Verify it’s actually being used — check your server logs at startup for a sharp version line, or look for squoosh warnings, which mean it isn’t installed correctly for your platform (a common miss in Docker: sharp ships platform-specific binaries, and installing on macOS then copying node_modules into a Linux container gives you a binary that silently fails).

The cache disappears on every redeploy

If you’re running in containers, .next/cache/images lives inside the container’s writable layer. Every deploy replaces the container, wiping the cache. Every image gets re-optimized from scratch after every deploy — expensive if you deploy often and have a large image library.

Mount it as a persistent volume:

services:
  app:
    image: myapp
    volumes:
      - image-cache:/app/.next/cache/images
volumes:
  image-cache:

Cap the damage from bad actors and layout bugs

next/image will generate a version for every distinct width your code requests. If a layout bug requests images at 17 slightly different widths across breakpoints, you’ve multiplied your cache size and CPU cost for no visual benefit. Set explicit, deliberate breakpoints in next.config.js:

module.exports = {
  images: {
    deviceSizes: [640, 828, 1200, 1920],
    imageSizes: [16, 32, 64, 128, 256],
    formats: ['image/webp'],
  },
}

Also restrict remotePatterns if you load images from external sources — an open image optimizer is a well-known way to turn your server into a free resizing proxy for anyone who finds the endpoint:

module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.yourdomain.com' },
    ],
  },
}

Or skip the built-in optimizer entirely

If images are already served from a CDN or object storage with its own resizing (Cloudflare Images, imgproxy, a bucket with a resize-on-request Lambda), don’t run two optimization layers. Set:

module.exports = {
  images: { unoptimized: true },
}

This serves images as-is and removes the CPU cost from your app server entirely — appropriate when the resizing job already lives somewhere else, not as a general fix for slow images.

The takeaway

Self-hosted, next/image optimization is a real CPU and disk cost your infrastructure pays, not Vercel’s. Install sharp correctly for your container’s platform, persist the image cache across deploys, and constrain deviceSizes/remotePatterns so the optimizer can’t be abused or over-generate variants.