---
title: "Image Optimization"
description: "Tune image quality, formats, and cache duration so Saturday's product photos are served efficiently without burning Image Optimization usage on every visit."
canonical_url: "https://vercel.com/academy/optimize-your-vercel-account/image-optimization"
md_url: "https://vercel.com/academy/optimize-your-vercel-account/image-optimization.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-07-22T03:34:40.175Z"
content_type: "lesson"
course: "optimize-your-vercel-account"
course_title: "Optimize Your Vercel Account"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Image Optimization

# 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.

\*\*Note: 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](https://github.com/vercel-labs/academy-optimize-vercel-account/blob/complete/next.config.ts) if you want to check your version against it.

The config:

```ts title="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:

```tsx title="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.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
