Image Optimization
Open any e-commerce site's product page in DevTools and look at the network panel. The largest things on the page are almost always the images. They're heavier than the HTML, the CSS, the JS, and the fonts combined. On Saturday, a single hero shot of the Foundry is bigger than every other asset on the page put together.
Vercel's Image Optimization handles the actual transcoding: convert to modern formats (AVIF, WebP), resize to fit the device, serve at appropriate quality. The defaults are sensible. The defaults also leave a lot of money on the table once you're paying attention. Two settings will get us back about half of what Saturday spends on image transformations.
Quality. And cache duration.
Outcome
Saturday's product images are served as AVIF or WebP at quality 75, cached for 30 days, and tagged with proper sizes hints so the browser downloads the right resolution for each viewport.
Hands-on exercise 4.1
Two files. The config flags and the per-image hints.
Requirements:
- Open
next.config.ts - Add an
imagesobject withformats: ['image/avif', 'image/webp']andminimumCacheTTL: 60 * 60 * 24 * 30(30 days) - Open
app/products/[slug]/page.tsx - On the product hero
<Image>, addquality={75}andsizes="(max-width: 768px) 100vw, 50vw" - (Optional but recommended) Do the same
qualityandsizestreatment on the product grid<Image>inapp/page.tsx. Use a smallersizesvalue for the grid since each thumbnail occupies a third of the row on desktop:sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" - Commit, push, redeploy
Implementation hints:
formatsis order-sensitive. AVIF first gives better compression for browsers that support it; WebP is the fallback. Both are dramatically smaller than the JPEGs inpublic/products/after transcoding. One tradeoff to know about: configuring both formats means Vercel caches an AVIF and a WebP variant for each size, so you pay a few extra transformations up front for smaller files on every delivery. With a 30-day TTL, that trade is worth it.minimumCacheTTLis the floor for how long a transformed image stays in Vercel's CDN cache. The default in Next.js 16 is 4 hours, which means a product photo that hasn't changed since the shoot can still get re-transformed six times a day. 30 days is a much more realistic floor for product photography that rarely changes.quality={75}is the default innext/image. Setting it explicitly is good practice because it documents the intent. If you want to be aggressive, 60 still looks fine for most product photography and saves another 15-25% per image. One catch: Next.js 16 only serves qualities you've allowlisted in the config, and the default list is just[75]. Addqualities: [60, 75]to theimagesobject first, or yourquality={60}silently snaps back to 75.sizesis the hint to the browser about how much screen the image will fill. Without it, Next.js downloads the largest variant it built, which is wasteful for thumbnails. With it, the browser picks the smallest variant that meets the target width.
Try It
After the redeploy, load a product page in a fresh browser tab. Open DevTools > Network and filter by Img.
You should see something like:
foundry.jpg?w=1200&q=75 200 image/avif 84 KB cached: 30 days
Three things to verify in that row:
- Content-type is
image/avif(orimage/webpif you're testing in Safari, which gets WebP). Old format isimage/jpeg. If you still see JPEG, the new config hasn't landed or the browser is using a cached older version. - Cache-Control header includes a long max-age. Click into the request and check the response headers. You're looking for something like
max-age=2592000(30 days in seconds). - File size is dramatically smaller than the source. The raw
foundry.jpginpublic/products/is 432 KB. The optimized AVIF should be around 80-100 KB.
The first load fills the CDN cache. Load the same page in another browser, or from another device, and the optimized image should be served immediately without re-transforming.
To see the multi-size hint at work, resize the browser window narrow (under 768px) and reload. You should see a different w= parameter in the URL, pulling a smaller variant.
Done-When
next.config.tshasimages.formatsandimages.minimumCacheTTLset- Product hero
<Image>hasquality={75}and asizesprop - (Optional) Product grid images also have
qualityandsizeshints - DevTools confirms images are served as AVIF or WebP
- DevTools confirms
max-ageon the image response is 30 days
Troubleshooting
The browser still loads JPEGs.
Three possibilities. The browser cached the JPEG from before the change (hard refresh, or use a private window). The deployment didn't rebuild after the config change (check the deployment timestamp). The accept header on the request doesn't include image/avif because the browser doesn't support it (Safari uses WebP, Chrome uses AVIF, both should be fine; very old browsers fall back to JPEG and that's correct behavior).
Image Optimization usage didn't drop.
Give it time. The savings come from the long cache TTL, which only manifests as repeat traffic hits cached entries instead of re-transforming. Right after the change you'll see a brief bump as the cache fills with the new variants, then a sustained drop.
The image looks blurry.
The sizes value might be wrong for the layout, causing the browser to pick a smaller variant than the displayed size. The rule of thumb: sizes should describe the displayed width at each breakpoint, not the source file width.
Solution
The finished next.config.ts is on the complete branch if you want to check your version against it.
The config:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
images: {
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
},
};
export default nextConfig;The hero image:
<Image
src={product.heroImage}
alt={`${product.name} ${product.colorway}`}
fill
quality={75}
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
priority
/>If you want to measure the actual savings, take a screenshot of Saturday > Observability > Image Optimization usage today, wait a week, and compare. On a small site you'll see usage halve. On a high-traffic site, the drop is closer to two-thirds.
Up next: images are tuned. Let's look at the requests themselves.
Was this helpful?