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:
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [],
},formatsnegotiates modern formats per the request'sAcceptheader.remotePatternsis 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:
| Phase | Behavior |
|---|---|
| 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-typeimage/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.
Related
- 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.
Bun & the compiled executable
knext's default build compiles your app into a single executable — bundled, minified, and bytecode-precompiled, with the Bun runtime baked in.
Versioning & compatibility
What a knext version number promises, which releases get fixes, and how the CLI, the packages and the operator fit together.