Vercel Logo

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.

What is Image Optimization?

Vercel's on-demand image pipeline. Request an image through next/image and Vercel resizes it, converts it to a modern format, and caches the result on the CDN. You're billed for the transformation work, so fewer variants and longer cache lives mean lower cost.

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:

  1. Open next.config.ts
  2. Add an images object with formats: ['image/avif', 'image/webp'] and minimumCacheTTL: 60 * 60 * 24 * 30 (30 days)
  3. Open app/products/[slug]/page.tsx
  4. On the product hero <Image>, add quality={75} and sizes="(max-width: 768px) 100vw, 50vw"
  5. (Optional but recommended) Do the same quality and sizes treatment on the product grid <Image> in app/page.tsx. Use a smaller sizes value for the grid since each thumbnail occupies a third of the row on desktop: sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  6. Commit, push, redeploy

Implementation hints:

  • formats is order-sensitive. AVIF first gives better compression for browsers that support it; WebP is the fallback. Both are dramatically smaller than the JPEGs in public/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.
  • minimumCacheTTL is 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 in next/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]. Add qualities: [60, 75] to the images object first, or your quality={60} silently snaps back to 75.
  • sizes is 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:

  1. Content-type is image/avif (or image/webp if you're testing in Safari, which gets WebP). Old format is image/jpeg. If you still see JPEG, the new config hasn't landed or the browser is using a cached older version.
  2. 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).
  3. File size is dramatically smaller than the source. The raw foundry.jpg in public/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.ts has images.formats and images.minimumCacheTTL set
  • Product hero <Image> has quality={75} and a sizes prop
  • (Optional) Product grid images also have quality and sizes hints
  • DevTools confirms images are served as AVIF or WebP
  • DevTools confirms max-age on 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:

next.config.ts
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:

app/products/[slug]/page.tsx
<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?

supported.