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 cached before Vercel transforms it again. Saturday's photos are referenced by path strings like/products/foundry.jpg, so the cache lasts for this value or the source image'sCache-Controlmax-age, whichever is larger. The default is a few hours, and right now the Next.js Image docs and Vercel's Image Optimization page list different numbers for it. Either way, a product photo that hasn't changed since the shoot can get re-transformed several times a day. 30 days is a much more realistic floor for product photography that rarely changes. (Statically imported images follow different rules, and Vercel caches those for up to a month on its own.)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. Otherwisequality={60}snaps to the closest allowed value, 75, and the only hint is a warning in development.sizesis the hint to the browser about how much screen the image will fill. Without it, the browser assumes the image is as wide as the viewport (100vw) and downloads a variant far bigger than a thumbnail needs. With it, the browser picks the smallest variant that meets the target width.- The product hero uses
preload, which starts fetching the page's biggest image from the<head>before the browser reaches it in the body. If your copy of the starter still sayspriority, swap it: Next.js 16 deprecatedpriorityin favor ofpreload.
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:
image?url=%2Fproducts%2Ffoundry.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. Before this change it wasimage/webp, the only format Next.js turns on by default. If Chrome still gets WebP, the new config hasn't landed or the browser is using a cached older version. A browser that accepts neither format gets the original JPEG. - 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 in a browser that supports it
- DevTools confirms
max-ageon the image response is 30 days
Troubleshooting
Chrome still gets WebP instead of AVIF.
This is expected until the new config is live, because WebP is the default. If it persists, the browser may have cached the WebP from before the change (hard refresh, or use a private window), or the deployment didn't rebuild after the config change (check the deployment timestamp). Also check the request's accept header. If it doesn't list image/avif, Next.js serves WebP, and a browser that accepts neither gets the original JPEG. Both fallbacks are 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 in the complete folder 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"
preload
/>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?