Performance 25 min read

The Complete Responsive Images Guide

Master responsive images with srcset, sizes, and picture elements. Learn resolution switching, art direction, and modern implementation patterns for optimal web performance.

By ImageGuide Team · Published January 19, 2026 · Updated January 19, 2026
responsive imagessrcsetsizespicture elementperformanceart direction

Responsive images ensure users download only what they need—mobile users shouldn’t download desktop-sized images. This comprehensive guide covers everything from basic srcset to advanced art direction patterns.

Why Responsive Images Matter

Consider a typical page with a hero image:

DeviceViewportDPRNeeded SizeTypical JPEG
Mobile375px2x750px60 KB
Tablet768px2x1536px180 KB
Desktop1440px1x1440px160 KB
Desktop1920px2x3840px450 KB

Without responsive images, you serve the same file to everyone—either too large for mobile or too blurry for retina displays.

The Three Problems Solved

  1. Resolution switching: Different sizes of the same image for different viewport widths
  2. Density switching: Different resolutions for different pixel densities (1x, 2x, 3x)
  3. Art direction: Different images (crops, compositions) for different contexts

Understanding srcset

The srcset attribute tells browsers what image options are available.

Width Descriptors (w)

Tell the browser each image’s width in pixels:

<img
  src="image-800.jpg"
  srcset="
    image-400.jpg 400w,
    image-800.jpg 800w,
    image-1200.jpg 1200w,
    image-1600.jpg 1600w
  "
  sizes="100vw"
  alt="Hero image"
>

How browsers choose:

  1. Parse sizes to determine display width (e.g., 100vw = 375px on mobile)
  2. Multiply by device pixel ratio (375px × 2 = 750px)
  3. Select smallest image that satisfies the requirement (800w)

Density Descriptors (x)

For fixed-size images with different resolutions:

<img
  src="logo.png"
  srcset="
    logo.png 1x,
    logo@2x.png 2x,
    logo@3x.png 3x
  "
  width="200"
  height="50"
  alt="Company logo"
>

Use density descriptors when:

  • Image has a fixed display size
  • Only resolution varies, not layout
  • Simple logos, icons, avatars

Don’t use density descriptors when:

  • Image width changes with viewport
  • You need resolution switching (use w instead)

Width vs Density: When to Use Each

ScenarioUse Width (w)Use Density (x)
Full-width hero
Fluid grid images
Fixed-size logo
Avatar (always 48px)
Responsive product

Mastering the sizes Attribute

The sizes attribute tells the browser how wide the image will display at different viewport widths.

Syntax

sizes="[media condition] [length], [fallback length]"

Basic Examples

<!-- Full width on all viewports -->
sizes="100vw"

<!-- Full width on mobile, half on larger screens -->
sizes="(max-width: 768px) 100vw, 50vw"

<!-- Multiple breakpoints -->
sizes="
  (max-width: 480px) 100vw,
  (max-width: 768px) 80vw,
  (max-width: 1200px) 50vw,
  33vw
"

<!-- Fixed width at certain sizes -->
sizes="(min-width: 1024px) 400px, 100vw"

Length Values

UnitDescriptionExample
vwViewport width percentage100vw, 50vw
pxFixed pixels400px, 800px
calc()Calculated valuecalc(100vw - 32px)
emRelative to font size30em

Common Layout Patterns

Full-bleed image:

sizes="100vw"

Contained with padding:

sizes="calc(100vw - 32px)"

Max-width container (1200px):

sizes="(min-width: 1200px) 1200px, 100vw"

Two-column layout:

sizes="(min-width: 768px) 50vw, 100vw"

Three-column grid:

sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"

Card in sidebar:

sizes="(min-width: 1024px) 300px, (min-width: 768px) 50vw, 100vw"

Getting sizes Right

The sizes attribute must match your CSS layout. If they don’t match, browsers make suboptimal choices.

Common mistake:

/* CSS says image is 50% of container, max 600px */
.image { max-width: 600px; width: 50%; }
<!-- sizes says 100vw — WRONG! -->
<img sizes="100vw" ...>

<!-- Correct: matches CSS -->
<img sizes="(min-width: 1200px) 600px, 50vw" ...>

The Picture Element

The <picture> element enables art direction and format fallbacks.

Format Fallbacks

Serve modern formats with graceful degradation:

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero image">
</picture>

Browser selects the first supported format.

Combined with srcset

Full responsive implementation with multiple formats:

<picture>
  <source
    srcset="
      hero-400.avif 400w,
      hero-800.avif 800w,
      hero-1200.avif 1200w
    "
    sizes="100vw"
    type="image/avif"
  >
  <source
    srcset="
      hero-400.webp 400w,
      hero-800.webp 800w,
      hero-1200.webp 1200w
    "
    sizes="100vw"
    type="image/webp"
  >
  <img
    src="hero-800.jpg"
    srcset="
      hero-400.jpg 400w,
      hero-800.jpg 800w,
      hero-1200.jpg 1200w
    "
    sizes="100vw"
    alt="Hero image"
    width="1200"
    height="600"
  >
</picture>

Art Direction with Media Queries

Different images for different contexts:

<picture>
  <!-- Wide crop for desktop -->
  <source
    media="(min-width: 1024px)"
    srcset="hero-wide.jpg"
  >
  <!-- Square crop for tablet -->
  <source
    media="(min-width: 640px)"
    srcset="hero-square.jpg"
  >
  <!-- Tall/portrait crop for mobile -->
  <img src="hero-tall.jpg" alt="Hero image">
</picture>

Art Direction + Multiple Formats

<picture>
  <!-- Desktop: wide crop, modern formats -->
  <source
    media="(min-width: 1024px)"
    srcset="hero-wide.avif"
    type="image/avif"
  >
  <source
    media="(min-width: 1024px)"
    srcset="hero-wide.webp"
    type="image/webp"
  >
  <source
    media="(min-width: 1024px)"
    srcset="hero-wide.jpg"
  >

  <!-- Mobile: portrait crop, modern formats -->
  <source srcset="hero-portrait.avif" type="image/avif">
  <source srcset="hero-portrait.webp" type="image/webp">
  <img src="hero-portrait.jpg" alt="Hero image">
</picture>

Selecting Image Breakpoints

Minimum set (covers most cases):

320, 640, 960, 1280, 1920

Comprehensive set:

320, 480, 640, 768, 960, 1024, 1280, 1440, 1600, 1920, 2560

Calculating What You Need

Consider:

  • Your layout breakpoints
  • Maximum display size × maximum DPR
  • ~25% minimum difference between sizes

Formula:

Max image width = Max display width × Max DPR

Example: 800px display × 3x DPR = 2400px needed

Avoiding Redundancy

Don’t create sizes too close together:

<!-- BAD: Too many similar sizes -->
srcset="img-400.jpg 400w, img-420.jpg 420w, img-450.jpg 450w"

<!-- GOOD: Meaningful differences (~25%+) -->
srcset="img-400.jpg 400w, img-500.jpg 500w, img-640.jpg 640w"

Implementation Patterns

Full-Width Hero

<picture>
  <source
    srcset="
      hero-640.avif 640w,
      hero-960.avif 960w,
      hero-1280.avif 1280w,
      hero-1920.avif 1920w,
      hero-2560.avif 2560w
    "
    sizes="100vw"
    type="image/avif"
  >
  <source
    srcset="
      hero-640.webp 640w,
      hero-960.webp 960w,
      hero-1280.webp 1280w,
      hero-1920.webp 1920w,
      hero-2560.webp 2560w
    "
    sizes="100vw"
    type="image/webp"
  >
  <img
    src="hero-1280.jpg"
    srcset="
      hero-640.jpg 640w,
      hero-960.jpg 960w,
      hero-1280.jpg 1280w,
      hero-1920.jpg 1920w,
      hero-2560.jpg 2560w
    "
    sizes="100vw"
    alt="Hero image"
    width="1920"
    height="1080"
    fetchpriority="high"
  >
</picture>

Product Grid (3 Columns on Desktop)

<img
  src="product-400.jpg"
  srcset="
    product-200.jpg 200w,
    product-300.jpg 300w,
    product-400.jpg 400w,
    product-600.jpg 600w,
    product-800.jpg 800w
  "
  sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
  alt="Product name"
  width="600"
  height="600"
  loading="lazy"
>

Blog Post Featured Image

<img
  src="featured-800.webp"
  srcset="
    featured-400.webp 400w,
    featured-600.webp 600w,
    featured-800.webp 800w,
    featured-1200.webp 1200w
  "
  sizes="(min-width: 768px) 800px, 100vw"
  alt="Article featured image"
  width="800"
  height="450"
>

Fixed-Size Avatar

<img
  src="avatar.jpg"
  srcset="
    avatar.jpg 1x,
    avatar@2x.jpg 2x,
    avatar@3x.jpg 3x
  "
  alt="User avatar"
  width="48"
  height="48"
  loading="lazy"
>

Background Image (via CSS)

.hero {
  background-image: url('hero-mobile.jpg');
  background-size: cover;
  background-position: center;
  aspect-ratio: 16 / 9;
}

@media (min-width: 640px) {
  .hero {
    background-image: url('hero-tablet.jpg');
  }
}

@media (min-width: 1024px) {
  .hero {
    background-image: url('hero-desktop.jpg');
  }
}

/* Resolution switching */
@media (min-resolution: 2dppx) {
  .hero {
    background-image: url('hero-mobile@2x.jpg');
  }

  @media (min-width: 1024px) {
    .hero {
      background-image: url('hero-desktop@2x.jpg');
    }
  }
}

CSS image-set() for Backgrounds

.hero {
  background-image: image-set(
    url('hero.avif') type('image/avif'),
    url('hero.webp') type('image/webp'),
    url('hero.jpg') type('image/jpeg')
  );
}

/* With resolution variants */
.logo {
  background-image: image-set(
    url('logo.png') 1x,
    url('logo@2x.png') 2x,
    url('logo@3x.png') 3x
  );
}

Generating Responsive Images

Using Sharp (Node.js)

const sharp = require('sharp');

const widths = [320, 640, 960, 1280, 1920];
const formats = ['jpeg', 'webp', 'avif'];

async function generateResponsiveImages(inputPath, outputDir) {
  const filename = path.parse(inputPath).name;

  for (const width of widths) {
    for (const format of formats) {
      const outputPath = `${outputDir}/${filename}-${width}.${format}`;

      await sharp(inputPath)
        .resize(width)
        .toFormat(format, {
          quality: format === 'avif' ? 65 : 80
        })
        .toFile(outputPath);
    }
  }
}

Using ImageMagick

#!/bin/bash
# Generate responsive images

INPUT=$1
WIDTHS="320 640 960 1280 1920"

for width in $WIDTHS; do
  # JPEG
  convert "$INPUT" -resize "${width}x" -quality 80 "output/image-${width}.jpg"

  # WebP
  convert "$INPUT" -resize "${width}x" -quality 80 "output/image-${width}.webp"
done

Using Squoosh CLI

npx @squoosh/cli \
  --webp '{"quality":80}' \
  --avif '{"quality":65}' \
  --resize '{"width":800}' \
  input.jpg

Build Tool Integration

Vite with vite-imagetools:

// vite.config.js
import { imagetools } from 'vite-imagetools';

export default {
  plugins: [imagetools()]
};
// Usage in code
import heroUrl from './hero.jpg?w=400;800;1200&format=webp&as=srcset';

Next.js (built-in):

import Image from 'next/image';

<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  sizes="100vw"
  alt="Hero"
/>

Preventing Layout Shifts

Always include dimensions to prevent CLS (Cumulative Layout Shift).

Width and Height Attributes

<img
  src="photo.jpg"
  srcset="photo-400.jpg 400w, photo-800.jpg 800w"
  sizes="(min-width: 768px) 50vw, 100vw"
  width="800"
  height="600"
  alt="Photo"
>

CSS ensures proper responsive behavior:

img {
  max-width: 100%;
  height: auto;
}

Aspect Ratio with CSS

.image-wrapper {
  aspect-ratio: 16 / 9;
}

.image-wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

Container Technique

<div class="responsive-image" style="--aspect-ratio: 4/3">
  <img src="photo.jpg" alt="Photo" loading="lazy">
</div>
.responsive-image {
  position: relative;
  aspect-ratio: var(--aspect-ratio);
  background: #f0f0f0;
}

.responsive-image img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

Lazy Loading with Responsive Images

<!-- Above the fold: load immediately -->
<img
  src="hero.jpg"
  srcset="hero-640.jpg 640w, hero-1280.jpg 1280w"
  sizes="100vw"
  alt="Hero"
  fetchpriority="high"
>

<!-- Below the fold: lazy load -->
<img
  src="content.jpg"
  srcset="content-640.jpg 640w, content-1280.jpg 1280w"
  sizes="100vw"
  alt="Content image"
  loading="lazy"
  decoding="async"
>

Placeholder Pattern

<div class="image-container">
  <!-- Tiny placeholder (inline base64) -->
  <img
    src="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
    class="placeholder"
    alt=""
    aria-hidden="true"
  >
  <!-- Full image -->
  <img
    src="photo.jpg"
    srcset="photo-400.jpg 400w, photo-800.jpg 800w"
    sizes="100vw"
    alt="Photo"
    loading="lazy"
    class="full-image"
    onload="this.classList.add('loaded')"
  >
</div>
.image-container {
  position: relative;
  aspect-ratio: 16 / 9;
}

.placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(20px);
  transform: scale(1.1);
}

.full-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.3s;
}

.full-image.loaded {
  opacity: 1;
}

Debugging Responsive Images

Chrome DevTools

  1. Network tab: Filter by “Img” to see which images loaded
  2. Responsive Design Mode: Test different viewport sizes
  3. Device emulation: Test different DPRs

Checking Which Source Was Selected

document.querySelectorAll('img').forEach(img => {
  console.log(`Display: ${img.clientWidth}x${img.clientHeight}`);
  console.log(`Natural: ${img.naturalWidth}x${img.naturalHeight}`);
  console.log(`Current src: ${img.currentSrc}`);
});

Visualizing sizes Accuracy

// Compare stated sizes with actual display
document.querySelectorAll('img[sizes]').forEach(img => {
  const stated = img.sizes;
  const actual = img.clientWidth;
  const natural = img.naturalWidth;
  const dpr = window.devicePixelRatio;
  const needed = actual * dpr;

  console.log({
    stated,
    actual,
    natural,
    needed,
    efficiency: `${Math.round(needed / natural * 100)}%`
  });
});

Common Mistakes

Mistake 1: Missing sizes Attribute

<!-- WRONG: Browser can't make informed choice -->
<img srcset="img-400.jpg 400w, img-800.jpg 800w" src="img.jpg" alt="Image">

<!-- CORRECT: Browser knows display size -->
<img
  srcset="img-400.jpg 400w, img-800.jpg 800w"
  sizes="(min-width: 768px) 50vw, 100vw"
  src="img.jpg"
  alt="Image"
>

Mistake 2: Mixing w and x Descriptors

<!-- WRONG: Can't mix descriptors -->
<img srcset="img-400.jpg 400w, img@2x.jpg 2x" src="img.jpg" alt="Image">

<!-- CORRECT: Use one or the other -->
<img srcset="img-400.jpg 400w, img-800.jpg 800w" sizes="..." src="img.jpg" alt="Image">
<!-- OR -->
<img srcset="img.jpg 1x, img@2x.jpg 2x" src="img.jpg" alt="Image">

Mistake 3: sizes Doesn’t Match Layout

/* Image displays at 300px on desktop */
.product-image { max-width: 300px; }
<!-- WRONG: sizes says 100vw -->
<img sizes="100vw" srcset="..." src="..." alt="Product">

<!-- CORRECT: sizes matches CSS -->
<img sizes="(min-width: 768px) 300px, 100vw" srcset="..." src="..." alt="Product">

Mistake 4: Missing Width and Height

<!-- WRONG: Causes layout shift -->
<img srcset="..." sizes="..." src="..." alt="Image">

<!-- CORRECT: Include dimensions -->
<img srcset="..." sizes="..." src="..." width="800" height="600" alt="Image">

Mistake 5: Too Many Similar Sizes

<!-- WRONG: Marginal differences, wasted bandwidth -->
srcset="img-400.jpg 400w, img-420.jpg 420w, img-450.jpg 450w, img-480.jpg 480w"

<!-- CORRECT: Meaningful jumps (~25%) -->
srcset="img-400.jpg 400w, img-500.jpg 500w, img-640.jpg 640w, img-800.jpg 800w"

Mistake 6: Not Testing on Real Devices

Browser DevTools device emulation doesn’t always match real device behavior. Test on:

  • Real phones at different network speeds
  • Different pixel densities (1x, 2x, 3x)
  • Various viewport sizes

Framework-Specific Implementation

React

function ResponsiveImage({ src, alt, sizes }) {
  return (
    <img
      src={`${src}-800.jpg`}
      srcSet={`
        ${src}-400.jpg 400w,
        ${src}-800.jpg 800w,
        ${src}-1200.jpg 1200w
      `}
      sizes={sizes}
      alt={alt}
      loading="lazy"
      decoding="async"
    />
  );
}

Vue

<template>
  <img
    :src="`${src}-800.jpg`"
    :srcset="`
      ${src}-400.jpg 400w,
      ${src}-800.jpg 800w,
      ${src}-1200.jpg 1200w
    `"
    :sizes="sizes"
    :alt="alt"
    loading="lazy"
    decoding="async"
  >
</template>

<script setup>
defineProps(['src', 'alt', 'sizes']);
</script>

Astro

---
import { getImage } from 'astro:assets';

const { src, alt, sizes } = Astro.props;

const widths = [400, 800, 1200];
const images = await Promise.all(
  widths.map(w => getImage({ src, width: w }))
);
---

<img
  src={images[1].src}
  srcset={images.map((img, i) => `${img.src} ${widths[i]}w`).join(', ')}
  sizes={sizes}
  alt={alt}
  loading="lazy"
  decoding="async"
/>

Summary

Quick Reference

Scenariosrcset TypeNeed sizes?Need picture?
Fluid image, same aspectWidth (w)YesFor formats only
Fixed size, retinaDensity (x)NoFor formats only
Different crops per viewportWidth (w)YesYes (art direction)
Format fallbacksWidth (w)YesYes

Implementation Checklist

  1. ✅ Use srcset with width descriptors for fluid images
  2. ✅ Use sizes that matches your CSS layout
  3. ✅ Include width and height attributes
  4. ✅ Use <picture> for format fallbacks (AVIF → WebP → JPEG)
  5. ✅ Use <picture> with media queries for art direction
  6. ✅ Generate images at appropriate breakpoints (~25% apart)
  7. ✅ Use loading="lazy" for below-fold images
  8. ✅ Test on real devices with various DPRs
  9. ✅ Verify sizes accuracy with DevTools
  10. ✅ Monitor Core Web Vitals impact

Responsive images require upfront work but deliver significant performance improvements. Start with basic srcset/sizes, add format fallbacks with picture, and implement art direction where it matters most.

Related Resources

Format References

Platform Guides

Ready to optimize your images?

Sirv automatically optimizes, resizes, and converts your images. Try it free.

Start Free Trial