---
title: "Next.js: Image Optimization"
slug: "nextjs-image-optimization"
category: "frontend"
tags: ["frontend", "nextjs", "images", "performance", "lcp"]
status: "stable"
last_updated: 2026-08-14
summary: "next/image in Next.js 16: the sizes attribute, priority for LCP, loaders, placeholder strategies, remotePatterns, and image hosting trade-offs."
related: ["[[frontend/nextjs]]", "[[frontend/nextjs-app-router]]", "[[frontend/nextjs-caching]]", "[[seo/image-seo]]", "[[seo/page-speed]]", "[[seo/core-web-vitals]]", "[[ops/vercel]]"]
---

## Overview

`next/image` is the right default for almost every image in a Next.js app. It generates responsive `srcset`, picks the modern format the browser supports, reserves layout space to prevent CLS, and lazy-loads off-screen images. The defaults are good; the failures all come from missing the right `sizes` value, forgetting `priority` on the LCP, or trusting unbounded remote sources.

## Use `next/image`, not the raw `<img>` tag

Import `Image` from `next/image`. Set `src`, `alt`, and either `width`/`height` (for known dimensions) or `fill` (for responsive containers).

```tsx
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Mountain at dawn"
      width={1600}
      height={900}
      priority
      sizes="(max-width: 768px) 100vw, 1280px"
    />
  );
}
```

The component reserves the box using the `width`/`height` aspect ratio, so the layout does not shift when the image loads. `alt` is required for accessibility (see [[frontend/accessibility]]); the empty string `""` is the right value only for decorative images.

## Set `sizes` to match the rendered width

The single most-missed prop is `sizes`. Without it, the browser downloads the biggest candidate from `srcset`. With it, the browser picks the right size for the viewport.

```tsx
// Full-width hero on mobile; max 1280px on desktop.
<Image src="/hero.jpg" alt="..." fill sizes="(max-width: 768px) 100vw, 1280px" />

// A 320px thumbnail at all sizes.
<Image src="/thumb.jpg" alt="..." width={320} height={240} sizes="320px" />

// Three columns on desktop, two on tablet, one on mobile.
<Image src="/card.jpg" alt="..." fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" />
```

The value is a CSS media query list with the rendered width. Match it to the layout, not to the source image. A wrong `sizes` either over-downloads (slow) or under-downloads (blurry).

## Mark the LCP with `priority`

The largest image visible above the fold is the Largest Contentful Paint candidate. Set `priority` on it so the framework preloads it and skips lazy loading.

```tsx
<Image src={post.cover} alt={post.title} fill priority sizes="100vw" />
```

`priority` issues a `<link rel="preload">` and `fetchpriority="high"`. Use it on exactly one image per page: the hero. Setting it on every image cancels the benefit. The LCP target is under 2.5s on a mid-tier mobile; see [[seo/core-web-vitals]].

## Pick a placeholder strategy

The component supports three placeholders: none (default), a blurred preview, or a static color. For static assets that import directly, Next.js generates a `blurDataURL` at build time.

```tsx
import cover from "./cover.jpg";
<Image src={cover} alt="..." placeholder="blur" />
```

For remote images, pass an explicit `blurDataURL` (a base64-encoded tiny image) or skip the placeholder and accept a brief gray box. Do not generate `blurDataURL` at request time on the Edge runtime; the cost is real.

For above-the-fold images with `priority`, skip the placeholder. The image will load before the placeholder is useful.

## Whitelist remote sources with `remotePatterns`

By default, `next/image` refuses to optimize remote URLs. Add hosts to `remotePatterns` in `next.config.ts`:

```ts
// next.config.ts
export default {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "images.example.com", pathname: "/**" },
      { protocol: "https", hostname: "cdn.example.com" },
    ],
  },
};
```

Never use `domains` (the legacy field) or a wildcard host. An attacker can use an unbounded image optimizer as a free egress proxy. Restrict the pathname when you can.

## Choose a loader that fits the workload

The default loader runs on the host (Vercel: built in). Two alternatives:

- A custom loader that proxies to an image CDN (Cloudinary, Imgix, Bunny, Cloudflare Images). Set `loader` and `loaderFile` in `next.config.ts`.
- `loader: "custom"` on a single component when one image needs a special URL shape.

```ts
// next.config.ts
export default {
  images: {
    loader: "custom",
    loaderFile: "./lib/image-loader.ts",
  },
};
```

Pick a CDN loader when the host's image optimizer is expensive (Vercel charges per source image) or when the asset library lives in a CMS that ships its own variants. Otherwise the default loader is fine.

## Watch the cost on Vercel; consider an external host at scale

The Vercel image optimizer is metered. For a marketing site with a few hundred unique sources it is free; for a UGC app with thousands of new uploads a day it is not.

- On Vercel, monitor "Optimized Image" usage in the dashboard.
- For UGC at scale, move to Cloudflare Images, Bunny, or Cloudinary and point `loader` at them. The per-image cost is lower and the cache is global.
- For static product photography, pre-render variants at build time and serve them as static assets. The optimizer is overkill for known sizes.

The right choice depends on the source library's size, churn, and how many distinct sizes the layout needs. See [[seo/image-seo]] for the SEO side and [[seo/page-speed]] for the LCP target.

## Related

- [[frontend/nextjs]]
- [[frontend/nextjs-app-router]]
- [[frontend/nextjs-caching]]
- [[seo/image-seo]]
- [[seo/page-speed]]
- [[seo/core-web-vitals]]
- [[ops/vercel]]
