Unexpected high number of image optimizations #81893
-
SummaryHi! I have a use case where I need to display a gallery of high-resolution images. To reduce bandwidth usage, I'm using image optimization to serve smaller versions with less quality when rendering the gallery. However, the images are private and must be fetched via pre-signed URLs. To handle this, I implemented a middleware that intercepts requests to paths like /presigned/:path*. It extracts the key from the URL (e.g., /presigned/a/b/c becomes a/b/c), generates a pre-signed URL, and redirects to it. This allows me to keep the src attribute of the image fixed. The same middleware also handles requests to /_next/image and ensures only authenticated users can access optimized images by validating the auth cookie. This setup works: in the browser dev tools, I can see that image requests go to /_next/image?url=/presigned/key, and the optimized versions are correctly served on repeat visits (most of the times they are actually served from memory/disk cache). However, according to Vercel’s dashboard, the number of image optimizations is surprisingly high. For instance, with just 10 images in the gallery, I see over 150 transformations in 12 hours. I expected the number of optimizations to roughly match the number of images since they need to be optimized just once. I've been wondering if that's considered "normal" behavior and if so why, or if the presigned url redirection may be causing unexpected cache misses. I'm leaving the code of my Image component and the code of my middleware in case it helps. Thanks! Additional information// The image component
<Image
src={`/presigned/${image.imageKey}`}
alt={image.name}
loading="lazy"
placeholder="empty"
quality={50}
width={400}
height={400}
className="object-cover w-full h-full"
draggable={false}
onError={() => {
statusRef.current = "error";
}}
/>
// The middleware
export const middleware = auth(async (req) => {
const auth = req.auth;
const { pathname, searchParams } = req.nextUrl;
if (pathname.startsWith("/presigned")) {
if (!auth) return new NextResponse("Unauthorized", { status: 401 });
const key = extractKey(pathname); // returns the key of the object
const url = await getPresignedUrl(key);
return NextResponse.rewrite(url);
}
if (
pathname.startsWith("/_next/image") &&
searchParams.get("url")?.startsWith("/presigned") &&
!auth
) {
return new NextResponse("Unauthorized");
}
return NextResponse.next();
});
export const config = {
matcher: ["/_next/image", "/presigned/:path*"],
}; |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 4 replies
-
You’re seeing a high number of image optimizations because each time someone requests a private image, your backend creates a new pre-signed URL (which includes a unique signature and expiry). Even though it’s always the same image, the URL is slightly different every time due to the way pre-signed URLs work. Vercel’s image optimizer uses the image URL as the “key” for its cache. So, if the URL changes, even a little bit it thinks it’s a brand new image, and will re-optimize it, instead of serving the cached optimized version. That’s why you’re seeing way more optimizations than the number of actual images. Is this normal? How do we fix it? |
Beta Was this translation helpful? Give feedback.
Hey, you’ve done a fantastic job digging into this! This is one of those tricky corners of the Next.js + Vercel ecosystem where things can get a little frustrating, especially when mixing authentication, private images, and the built-in image optimizer.
The optimizer essentially acts like a new client. It fetches your image using only the info in the 'src' URL with no auth. This is by design for performance/scalability, but it does make protecting private images tough.
You would need to optimize images before upload, or use something like CloudFront (you mentioned) or a dedicated image proxy with signed URLs
There is no “perfect” way to have both secure, authenticated private images and u…