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).
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 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.
// 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.
<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 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.
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:
// 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
loaderandloaderFileinnext.config.ts. loader: "custom"on a single component when one image needs a special URL shape.
// 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
loaderat 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 image-seo for the SEO side and page-speed for the LCP target.