Next.js Image Optimization When Self-Hosting
08 Jan 2026next/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.