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 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's Cache-Control max-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 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. Otherwise quality={60} snaps to the closest allowed value, 75, and the only hint is a warning in development.
  • sizes is 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 says priority, swap it: Next.js 16 deprecated priority in favor of preload.

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:

  1. Content-type is image/avif. Before this change it was image/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.
  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 in a browser that supports it
  • DevTools confirms max-age on 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:

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"
  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?

supported.