knext

Image optimization

next/image optimization via sharp, with optimized variants persisted to the object store so they survive scale-to-zero.

knext serves next/image runtime optimization (the /_next/image endpoint: resize, quality, and format negotiation to AVIF/WebP) using sharp. Optimization is configured in next.config.ts:

next.config.ts
images: {
  formats: ['image/avif', 'image/webp'],
  remotePatterns: [],
},
  • formats negotiates modern formats per the request's Accept header.
  • remotePatterns is an explicit allowlist. Empty (the default) means local images only — there is no open optimizer, so the endpoint cannot be turned into an SSRF proxy. Add trusted hosts as needed.

Variants survive scale-to-zero

Optimizing an image is CPU-expensive, and under Knative scale-to-zero every cold pod starts with an empty local cache. Without intervention, each freshly-woken pod would re-optimize images another pod already produced — burning the cold-start CPU budget on redundant work.

knext closes this with an object-store sync for the optimized-variant cache. The optimized variants persist to the object store (GCS / S3 / MinIO — knext's data plane), so a variant computed once is reused by every later pod. This is the same "survives the cold start" property as the bytecode cache: the work outlives the pod that did it.

How the sync works

Next.js writes optimized variants to a pod-local directory (.next/cache/images/<cacheKey>/...), where cacheKey is a content hash of (src, width, quality, mimeType). On the pinned Next.js version the image-optimizer cache is not pluggable through cacheHandler, so knext syncs the directory Next already writes rather than intercepting it:

PhaseBehavior
Restore (startup)Downloads every persisted variant from the store into the local cache dir, skipping files that already exist locally — warming the cache before the server starts.
Watch + push (runtime)Watches the cache dir; when Next writes a new variant, the (debounced) variant directory is uploaded to the store under <prefix>/<cacheKey>/.

The sync is guarded by STORAGE_BUCKET: when it is unset, the sync is a no-op and Next falls back to pod-local caching. Both restore and push are best-effort — a store outage degrades to local-only optimization rather than crashing the pod.

Shipping sharp with your image

Your app compiles to a single executable, and sharp is not entirely JavaScript — the part that actually resizes pixels is a native library. A compiled executable can only load code that was built into it, so that native library has to travel as a real file next to the binary, in a native/ directory.

kn-next build creates that directory for you, and the generated Dockerfile copies it in. You only need to know this exists if you write your own Dockerfile or copy the binary somewhere by hand.

The native library is platform-specific, and the platform that matters is the container image's (Linux/musl), not your laptop's. kn-next build stages the image's addon set regardless of where you build: when your local install already has it (a Linux host) it is copied, and when it does not (a macOS host holds only macOS addons) the build downloads the version your app's installed sharp resolves to, as pinned in bun.lock, and verifies the download against the lockfile's integrity hash before staging it. If neither your install nor your lockfile has the image's addon, the build fails and names the missing package — it never ships an image that cannot load sharp.

At boot, the binary picks the addon matching the platform it is actually running on — never just the first directory it finds — and if nothing staged matches, it fails with an error naming the platforms involved instead of crashing on an unreadable binary. If you need to override the selection (say, a hand-built image with a custom layout), set KNEXT_SHARP_ADDON to the absolute path of the .node file inside its @img tree.

Upgrading: if you built an app image on macOS before this behaviour existed, the old addon selection is baked into that binary — rebuild and redeploy with the current kn-next build to get working image optimization; no configuration change is needed.

If your app imports sharp and its addon cannot load, the pod does not start — the generated server entry imports sharp at boot, so a missing or wrong-platform addon is a startup failure with an error naming the cause, not a silent degradation. Only an app that never pulls sharp into its bundle (no next/image) runs with an empty native/; for such an app /_next/image answers 200 and returns the original image, unresized.

The quickest check against a running container:

curl -sI -H 'Accept: image/avif,image/webp' \
  'http://localhost:3000/_next/image?url=%2Fyour-image.png&w=256&q=75' | grep -i content-type

image/avif or image/webp means optimization is working. Getting back image/png — the format you asked it to convert from — means sharp could not load, and the endpoint is passing the original through.

Verified, not asserted. Every build runs a check that starts the real production container, requests an optimized image, and fails the build unless it comes back as a modern format and smaller than the source. The check also tests itself against a deliberately broken optimizer first, so it cannot pass by accident.

  • Scale to zero — why a cold pod must not re-do work, and the autoscaling model.
  • Bytecode cache — the sibling "survives the cold start" feature for V8 bytecode.
  • Multi-cloud — the object store (GCS / S3 / MinIO) that holds the persisted variants.

On this page