Learn

/

Responsive Images

Responsive Images

5 patterns

srcSet, sizes, next/image, art direction with <picture>, and avoiding layout shift. You'll hit this when a 2 MB hero image loads on a phone with a slow connection.

Avoid
<img
  src="/hero.jpg"
  alt="Hero image"
  style={{ width: "100%", height: "auto" }}
/>
<img
  src="/hero.jpg"
  alt="Hero image"
  style={{ width: "100%", height: "auto" }}
/>

Prefer
<img
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={630}
  style={{ width: "100%", height: "auto" }}
/>
<img
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={630}
  style={{ width: "100%", height: "auto" }}
/>
Why avoid

Without dimensions, the browser doesn't know how tall the image will be until it downloads. The page content shifts downward when the image loads, causing a poor CLS score and a jarring user experience.

Why prefer

Setting width and height attributes lets the browser calculate the aspect ratio and reserve space before the image loads. Combined with width: 100% and height: auto, the image scales responsively while preventing Cumulative Layout Shift (CLS).

web.dev: Optimize CLS
Avoid
<img
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
/>
<img
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
/>

Prefer
import Image from "next/image";

<Image
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, 50vw"
/>
import Image from "next/image";

<Image
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, 50vw"
/>
Why avoid

A plain <img> serves the same 800px image to every device. A mobile user downloads 4x more pixels than needed. No lazy loading, no modern format negotiation, no srcSet. next/image handles all of this.

Why prefer

Next.js Image automatically generates srcSet with multiple resolutions, serves WebP/AVIF, lazy-loads by default, and prevents layout shift. The sizes prop tells the browser how wide the image will be at each viewport, so it downloads the right size.

Next.js: Image component
Avoid
// Same crop for all screen sizes
<Image
  src="/hero-wide.jpg"
  alt="Team photo"
  width={1600}
  height={600}
  sizes="100vw"
/>
// Same crop for all screen sizes
<Image
  src="/hero-wide.jpg"
  alt="Team photo"
  width={1600}
  height={600}
  sizes="100vw"
/>

Prefer
<picture>
  <source
    media="(max-width: 640px)"
    srcSet="/hero-portrait.jpg"
    width={640}
    height={800}
  />
  <source
    media="(max-width: 1024px)"
    srcSet="/hero-square.jpg"
    width={1024}
    height={1024}
  />
  <img
    src="/hero-wide.jpg"
    alt="Team photo"
    width={1600}
    height={600}
    style={{ width: "100%", height: "auto" }}
  />
</picture>
<picture>
  <source
    media="(max-width: 640px)"
    srcSet="/hero-portrait.jpg"
    width={640}
    height={800}
  />
  <source
    media="(max-width: 1024px)"
    srcSet="/hero-square.jpg"
    width={1024}
    height={1024}
  />
  <img
    src="/hero-wide.jpg"
    alt="Team photo"
    width={1600}
    height={600}
    style={{ width: "100%", height: "auto" }}
  />
</picture>
Why avoid

A 1600x600 panoramic image on a 375px phone becomes a tiny strip where you can't see the subject. Art direction means changing the crop/composition, not just the resolution. Use <picture> when the image needs different framing at different sizes.

Why prefer

Art direction uses <picture> to serve different crops for different screens. A wide panoramic hero on desktop becomes a tall portrait crop on mobile, keeping the subject visible. srcSet alone only changes resolution, not composition.

MDN: Art direction
Avoid
<Image
  src="/card.jpg"
  alt="Card image"
  width={400}
  height={300}
  sizes="100vw"
/>

{/* Image is actually in a 3-column grid */}
<Image
  src="/card.jpg"
  alt="Card image"
  width={400}
  height={300}
  sizes="100vw"
/>

{/* Image is actually in a 3-column grid */}

Prefer
<Image
  src="/card.jpg"
  alt="Card image"
  width={400}
  height={300}
  sizes="(max-width: 640px) 100vw,
         (max-width: 1024px) 50vw,
         33vw"
/>
<Image
  src="/card.jpg"
  alt="Card image"
  width={400}
  height={300}
  sizes="(max-width: 640px) 100vw,
         (max-width: 1024px) 50vw,
         33vw"
/>
Why avoid

sizes="100vw" tells the browser this image fills the viewport. But in a 3-column grid, each image is only ~33% of the viewport. The browser downloads a 1440px-wide image when a 480px-wide one would suffice, wasting 3x the bandwidth.

Why prefer

The sizes attribute tells the browser how wide the image will display at each viewport width *before* CSS loads. With 100vw, a card in a 3-column grid triggers downloading a full-width image. Accurate sizes lets the browser pick the right resolution from the srcSet.

MDN: img sizes attribute
Avoid
.hero {
  background-image: url("/hero-4k.jpg");
  background-size: cover;
  background-position: center;
  min-height: 60vh;
}
.hero {
  background-image: url("/hero-4k.jpg");
  background-size: cover;
  background-position: center;
  min-height: 60vh;
}

Prefer
.hero {
  background-image: url("/hero-640.jpg");
  background-size: cover;
  background-position: center;
  min-height: 60vh;
}

@media (min-width: 640px) {
  .hero {
    background-image: url("/hero-1024.jpg");
  }
}

@media (min-width: 1024px) {
  .hero {
    background-image:
      image-set(
        url("/hero-1920.avif") type("image/avif"),
        url("/hero-1920.webp") type("image/webp"),
        url("/hero-1920.jpg") type("image/jpeg")
      );
  }
}
.hero {
  background-image: url("/hero-640.jpg");
  background-size: cover;
  background-position: center;
  min-height: 60vh;
}

@media (min-width: 640px) {
  .hero {
    background-image: url("/hero-1024.jpg");
  }
}

@media (min-width: 1024px) {
  .hero {
    background-image:
      image-set(
        url("/hero-1920.avif") type("image/avif"),
        url("/hero-1920.webp") type("image/webp"),
        url("/hero-1920.jpg") type("image/jpeg")
      );
  }
}
Why avoid

A 4K background image on a 375px phone downloads megabytes of unnecessary pixels. Unlike <img srcSet>, CSS background-image has no built-in resolution switching, so you must use media queries to serve appropriate sizes.

Why prefer

Background images can't use srcSet, so media queries swap resolution manually. image-set() provides format negotiation (AVIF > WebP > JPEG). Mobile users get a 640px image instead of a 4K one, saving up to 10x the file size.

MDN: image-set()