Krunkit
Responsive
HTML
Performance

The Complete Guide to Responsive Images in 2026: srcset, sizes, and the picture Element

Everything you need to know about responsive images — srcset, sizes attribute, art direction with the picture element, choosing breakpoints, DPR handling, performance budgets, and practical code examples.

Krunkit Team··16 min read

Responsive images are one of the most impactful performance techniques available to web developers, yet they remain widely misunderstood and underused. A 2025 HTTP Archive analysis found that over 60% of image-heavy websites still serve the same image size to all devices — phones and 4K monitors alike.

The result is predictable: mobile users download images 3-8x larger than their screens can display, wasting bandwidth and slowing page loads. On the flip side, high-DPI desktop users sometimes get low-resolution images that look blurry.

This guide covers everything you need to implement responsive images correctly: the srcset attribute, the sizes attribute, the <picture> element for art direction, breakpoint selection, device pixel ratio handling, and performance budgeting.

The Core Problem

Consider a hero image that's 1600px wide on desktop. On a standard-DPI desktop monitor, you need about 1600 physical pixels. On a 2x Retina display, you'd ideally serve 3200 pixels. On a mobile phone at 375px viewport width (even at 2x DPI), you only need 750 pixels.

If you serve the 3200px version to everyone:

  • Desktop (1x DPI): Downloads 3200px, displays 1600px. 4x more pixels than needed.
  • Mobile (2x DPI): Downloads 3200px, displays 750px. 18x more pixels than needed.

That 3200px JPEG might be 800 KB. The 750px version would be about 60 KB. A mobile user on a cellular connection downloads 740 KB of data they'll never see.

Responsive images solve this by letting you provide multiple versions and letting the browser choose the right one.

The srcset Attribute

The srcset attribute on an <img> tag provides the browser with a list of image sources and their sizes. There are two syntaxes: width descriptors and pixel density descriptors.

Width Descriptors (Most Common)

<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg 400w,
    hero-800.jpg 800w,
    hero-1200.jpg 1200w,
    hero-1600.jpg 1600w,
    hero-2400.jpg 2400w
  "
  sizes="100vw"
  alt="Mountain landscape at sunrise"
/>

The w descriptor tells the browser the intrinsic width of each image file in pixels. hero-400.jpg 400w means "this file is 400 pixels wide." The browser uses this information, combined with the sizes attribute and the device's pixel ratio, to select the most appropriate image.

The src attribute serves as the fallback for browsers that don't support srcset (effectively none in 2026, but it's still required for valid HTML).

Pixel Density Descriptors

<img
  src="logo-1x.png"
  srcset="
    logo-1x.png 1x,
    logo-2x.png 2x,
    logo-3x.png 3x
  "
  alt="Company logo"
/>

The x descriptor specifies the intended device pixel ratio for each image. logo-2x.png 2x means "serve this image on 2x DPI screens."

When to use which:

  • Width descriptors (w): For images whose display size changes with the viewport (hero images, content images, product photos). This is the more common and flexible approach.
  • Density descriptors (x): For fixed-size images that don't change with the viewport (logos, icons, avatars). Simpler but less flexible.

You cannot mix w and x descriptors in the same srcset.

The sizes Attribute

The sizes attribute is required when using width descriptors (w) in srcset. It tells the browser how wide the image will be displayed at different viewport sizes. Without this information, the browser can't calculate which source image to choose.

Syntax

<img
  srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  src="image-800.jpg"
  alt="Description"
/>

The sizes value is a comma-separated list of media conditions and corresponding lengths, evaluated left to right:

  1. (max-width: 640px) 100vw — On viewports up to 640px, the image fills the full width.
  2. (max-width: 1024px) 50vw — On viewports from 641px to 1024px, the image takes half the width.
  3. 33vw — The default (no media condition): on larger viewports, the image takes one-third width.

How the Browser Uses sizes

Here's the calculation the browser performs:

  1. Check the viewport width (e.g., 375px on iPhone).
  2. Evaluate sizes to determine display width: (max-width: 640px) 100vw matches, so display width = 375px.
  3. Multiply by device pixel ratio: iPhone at 3x DPI needs 375 * 3 = 1125 physical pixels.
  4. Select the smallest srcset source that covers 1125px: image-1200.jpg 1200w.

This happens before the image is downloaded, which is why the browser needs sizes as a hint — it can't inspect the CSS layout because CSS may not have loaded yet when the image request starts.

Common sizes Patterns

Full-width hero image:

sizes="100vw"

Image in a two-column layout (stacks on mobile):

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

Image in a three-column grid (two columns on tablet, one on mobile):

sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33.3vw"

Fixed-width image in a sidebar:

sizes="(max-width: 768px) 100vw, 300px"

Image with max-width container:

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

The sizes Accuracy Problem

There's an inherent limitation: sizes is a hint written in HTML, but the actual image display size is determined by CSS. If your CSS changes and your sizes values don't update to match, the browser may select the wrong image source.

In practice, moderate inaccuracy in sizes is acceptable. If the browser selects a slightly larger image than needed, you waste some bandwidth but the image looks fine. If it selects a slightly smaller one, the image may appear slightly soft but the browser can still upscale it.

Some frameworks (like Next.js) compute sizes from layout props automatically, reducing the chance of mismatch. If you're managing sizes manually, audit it when you change your layout CSS.

The picture Element: Art Direction

The <picture> element solves a different problem than srcset: art direction. Art direction means serving different image crops or compositions at different viewport sizes — not just different resolutions of the same image.

Basic Art Direction

<picture>
  <source
    media="(max-width: 640px)"
    srcset="hero-mobile-crop.jpg"
  />
  <source
    media="(max-width: 1024px)"
    srcset="hero-tablet-crop.jpg"
  />
  <img src="hero-desktop.jpg" alt="Product showcase" />
</picture>

On mobile, this serves a tightly cropped version of the image (perhaps just the product, without the surrounding context). On tablet, a medium crop. On desktop, the full wide composition.

This is different from responsive sizing. With srcset alone, you serve the same image at different resolutions. With <picture> and art direction, you can serve fundamentally different images.

Art Direction + Responsive Sizing Combined

You can combine art direction with srcset on each source:

<picture>
  <source
    media="(max-width: 640px)"
    srcset="
      hero-mobile-400.jpg 400w,
      hero-mobile-800.jpg 800w
    "
    sizes="100vw"
  />
  <source
    media="(max-width: 1024px)"
    srcset="
      hero-tablet-600.jpg 600w,
      hero-tablet-1200.jpg 1200w
    "
    sizes="100vw"
  />
  <img
    src="hero-desktop-1200.jpg"
    srcset="
      hero-desktop-800.jpg 800w,
      hero-desktop-1200.jpg 1200w,
      hero-desktop-1800.jpg 1800w,
      hero-desktop-2400.jpg 2400w
    "
    sizes="100vw"
    alt="Product showcase"
  />
</picture>

Each art direction breakpoint gets its own set of resolution variants. The browser first picks the appropriate source based on the media query, then selects the best resolution from that source's srcset.

Format Selection with picture

The <picture> element is also the standard way to serve modern image formats with fallbacks:

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

The browser evaluates <source> elements top to bottom and picks the first it supports. This works because type tells the browser the MIME type before it downloads the file.

Full Example: Format + Art Direction + Resolution

Here's a comprehensive example combining all three techniques:

<picture>
  <!-- Mobile: cropped, AVIF/WebP/JPEG, multiple resolutions -->
  <source
    media="(max-width: 640px)"
    type="image/avif"
    srcset="hero-mobile-400.avif 400w, hero-mobile-800.avif 800w"
    sizes="100vw"
  />
  <source
    media="(max-width: 640px)"
    type="image/webp"
    srcset="hero-mobile-400.webp 400w, hero-mobile-800.webp 800w"
    sizes="100vw"
  />
  <source
    media="(max-width: 640px)"
    srcset="hero-mobile-400.jpg 400w, hero-mobile-800.jpg 800w"
    sizes="100vw"
  />

  <!-- Desktop: full composition, AVIF/WebP/JPEG, multiple resolutions -->
  <source
    type="image/avif"
    srcset="hero-1200.avif 1200w, hero-1800.avif 1800w, hero-2400.avif 2400w"
    sizes="100vw"
  />
  <source
    type="image/webp"
    srcset="hero-1200.webp 1200w, hero-1800.webp 1800w, hero-2400.webp 2400w"
    sizes="100vw"
  />
  <img
    src="hero-1200.jpg"
    srcset="hero-1200.jpg 1200w, hero-1800.jpg 1800w, hero-2400.jpg 2400w"
    sizes="100vw"
    alt="Product showcase — full collection displayed on marble surface"
  />
</picture>

This is the most comprehensive approach but also the most verbose. In practice, most sites don't need all three dimensions of variation. Pick the ones that matter for your use case.

Choosing Breakpoints

A common mistake is choosing image breakpoints based on device-specific viewport widths (320, 375, 414, 768, 1024, 1440...). This approach is fragile because new devices with new viewport widths appear constantly.

Better Approach: File Size Steps

Choose breakpoints based on file size jumps rather than device widths. The goal is to ensure no user downloads an image more than ~20-30 KB larger than what they actually need.

Method:

  1. Start with your largest needed image (e.g., 2400px wide for 1200px display at 2x DPI).
  2. Compress it at your target quality. Note the file size.
  3. Progressively reduce the width and check file sizes.
  4. Create a new breakpoint whenever the file size drops by a significant step (roughly 20-30 KB for typical content images, or 50-80 KB for hero images).

Example for a hero photograph (WebP, quality 80):

| Width | File Size | Delta from Previous | |-------|-----------|-------------------| | 2400px | 312 KB | — | | 1800px | 198 KB | -114 KB | | 1200px | 108 KB | -90 KB | | 800px | 58 KB | -50 KB | | 400px | 19 KB | -39 KB |

These five variants provide good coverage. A user who needs 800px gets a 58 KB file instead of the 312 KB 2400px version — an 81% bandwidth saving.

Practical Breakpoint Sets

For most projects, these standard sets work well:

Minimal (3 variants): 400, 800, 1600 Good enough for many sites. Simple to maintain.

Standard (5 variants): 400, 800, 1200, 1600, 2400 Covers most display sizes and DPI combinations well.

Comprehensive (7 variants): 320, 640, 960, 1280, 1600, 1920, 2560 For image-heavy sites where maximum optimization matters.

Device Pixel Ratio (DPR) Considerations

Modern devices commonly have these pixel ratios:

| DPR | Examples | |-----|----------| | 1x | Older laptops, most external monitors | | 1.25-1.5x | Many Windows laptops (125-150% scaling) | | 2x | Retina MacBooks, many flagship phones | | 3x | iPhone Pro models, high-end Android phones |

When a 2x device displays an image at 400 CSS pixels wide, it ideally needs an 800px source image for crisp rendering. At 3x, it needs 1200px.

Do You Need to Serve 3x Images?

The short answer: usually not. Research from tech companies including Twitter and Shopify has shown that the perceptual quality difference between 2x and 3x images is minimal on phone-sized screens. The extra pixels are there, but at the viewing distance and screen size of a phone, most people can't distinguish them.

A practical approach:

  • Serve up to 2x for most content images. A 2x image on a 3x screen still looks good.
  • Serve up to 3x only for critical branding elements (logo, hero image) where crispness matters.
  • Never serve 1x if the image is important. Even on a 1x display, a slightly larger image provides some sharpness benefit and handles browser zoom.

Implementing DPR-Aware Sizing

When writing sizes, you don't need to account for DPR — the browser handles it automatically. If sizes evaluates to 400px and the device is 2x, the browser selects from srcset based on a 800px target.

This is one of the elegant aspects of the w descriptor: you describe the available image widths, and the browser factors in both display size and DPR to make the optimal selection.

Performance Budgets for Images

A performance budget defines the maximum allowed resource size for different page types. For images specifically:

Setting an Image Budget

  1. Define total page budget. A common target: under 1.5 MB total page weight for mobile.
  2. Allocate image share. Images typically account for 50-70% of page weight. Budget 750 KB - 1 MB for images.
  3. Account for lazy loading. The budget applies to initial load — images below the fold don't count if they're lazy loaded.
  4. Divide by image count. If your page has a hero image and 8 content images above the fold, the hero might get 200 KB and each content image gets ~70 KB.

Budget by Image Role

| Role | Budget (per image) | Typical Dimensions | |------|-------------------|-------------------| | Hero / Banner | 150-250 KB | 1200-1600px wide | | Content Image | 50-100 KB | 600-800px wide | | Thumbnail | 10-25 KB | 200-300px wide | | Avatar / Icon | 2-10 KB | 48-128px wide | | Background Texture | 20-50 KB | Tiling pattern |

Enforcing Budgets

Integrate budget checks into your CI/CD pipeline:

// Example: Lighthouse CI budget configuration
// lighthouserc.js
module.exports = {
  ci: {
    assert: {
      assertions: {
        'resource-summary:image:size': ['error', { maxNumericValue: 750000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
      },
    },
  },
};

Framework-Specific Implementation

Next.js (next/image)

Next.js provides the <Image> component that handles responsive images automatically:

import Image from 'next/image';

export function HeroImage() {
  return (
    <Image
      src="/images/hero.jpg"
      alt="Mountain landscape at sunrise"
      width={1600}
      height={900}
      sizes="100vw"
      priority
    />
  );
}

Next.js automatically:

  • Generates multiple size variants
  • Serves WebP/AVIF based on browser support (via Accept header)
  • Adds srcset with appropriate width descriptors
  • Lazy loads by default (use priority for above-fold images)
  • Sets width and height to prevent layout shift

The sizes prop works the same as the HTML attribute. If omitted, Next.js defaults to 100vw, which may cause the browser to download larger images than needed for non-full-width images.

Astro

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<Image
  src={heroImage}
  widths={[400, 800, 1200, 1600]}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"
  alt="Mountain landscape"
/>

Astro generates variants at build time and outputs a proper <img> with srcset.

Plain HTML (No Framework)

If you're generating image variants with a build tool or image processing service, the HTML is straightforward:

<img
  src="hero-1200.webp"
  srcset="
    hero-400.webp 400w,
    hero-800.webp 800w,
    hero-1200.webp 1200w,
    hero-1600.webp 1600w
  "
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"
  alt="Mountain landscape at sunrise"
  width="1600"
  height="900"
  loading="lazy"
  decoding="async"
/>

Note the decoding="async" attribute — this allows the browser to decode the image off the main thread, preventing image decoding from blocking rendering.

Generating Image Variants

You need a way to create the multiple sizes and formats. Several approaches:

Build-Time Generation

Use an image processing library in your build pipeline:

# Using sharp-cli to generate variants
for width in 400 800 1200 1600 2400; do
  sharp -i source.jpg -o "output-${width}.webp" resize $width -- webp
  sharp -i source.jpg -o "output-${width}.avif" resize $width -- avif
done

Image CDN Services

Services like Cloudinary, imgix, and Cloudflare Images generate variants on-the-fly via URL parameters:

<img
  srcset="
    https://cdn.example.com/hero.jpg?w=400&f=webp 400w,
    https://cdn.example.com/hero.jpg?w=800&f=webp 800w,
    https://cdn.example.com/hero.jpg?w=1200&f=webp 1200w
  "
  sizes="100vw"
  src="https://cdn.example.com/hero.jpg?w=1200"
  alt="Description"
/>

This is the simplest approach for teams that don't want to manage an image processing pipeline. The CDN generates, caches, and serves variants automatically.

Client-Side Resizing

For user-uploaded images in web applications, you can resize in the browser before upload. Tools like Krunkit use WebAssembly-based image processing to handle resizing and compression entirely on the client side — useful for preparing images before they enter your pipeline.

Common Mistakes and How to Fix Them

Mistake 1: srcset Without sizes

<!-- Wrong: browser assumes sizes="100vw" -->
<img srcset="img-400.jpg 400w, img-800.jpg 800w" src="img-800.jpg" alt="..." />

Without sizes, the browser assumes the image fills the full viewport width. If the image only takes up a sidebar column (say, 300px), the browser downloads a much larger image than needed.

Mistake 2: Using CSS to Hide Images on Mobile

/* Don't do this */
.desktop-only-image { display: none; }
@media (min-width: 768px) { .desktop-only-image { display: block; } }

The browser still downloads hidden images. Use <picture> with media queries instead, or loading="lazy" combined with proper responsive sizing.

Mistake 3: Forgetting width and height

<!-- Causes layout shift -->
<img src="photo.jpg" alt="Photo" />

<!-- Prevents layout shift -->
<img src="photo.jpg" alt="Photo" width="800" height="600" />

Always include width and height attributes. The browser uses these to calculate the aspect ratio and reserve space before the image loads, preventing Cumulative Layout Shift (CLS).

Mistake 4: Lazy Loading the LCP Image

<!-- Don't lazy-load your most important image -->
<img src="hero.jpg" alt="Hero" loading="lazy" />

The Largest Contentful Paint (LCP) element — usually the hero image — should load as early as possible. Add loading="eager" (or omit the attribute, as eager is the default) and consider adding fetchpriority="high":

<img src="hero.jpg" alt="Hero" fetchpriority="high" />

Mistake 5: Too Many Variants

Generating 15 size variants for every image creates build complexity, storage costs, and CDN cache fragmentation. Five variants (400, 800, 1200, 1600, 2400) covers the vast majority of real-world scenarios. More variants have diminishing returns.

Testing Your Responsive Images

Browser DevTools

Chrome DevTools lets you verify which image source the browser selected:

  1. Open DevTools and go to the Network tab.
  2. Filter by "Img" to see image requests.
  3. Check the actual file downloaded and its dimensions.
  4. Use device emulation to test at different viewport widths and DPR values.

Lighthouse Audit

Lighthouse includes a "Properly size images" audit that identifies images served at resolutions significantly larger than their display size. Run this on both mobile and desktop configurations.

RUM (Real User Monitoring)

For production sites, monitor actual image loading behavior across your real user base. Track which srcset sources are being selected, image load times, and LCP performance segmented by device type and connection speed.

Summary

Responsive images are not optional for performance-conscious sites. The technique isn't complex once you understand the mechanics:

  1. Use srcset with width descriptors for images that change size with the viewport.
  2. Always pair srcset (w) with sizes so the browser can make the right selection.
  3. Use <picture> when you need art direction (different crops) or format fallbacks.
  4. Generate 3-5 size variants based on file size steps, not device widths.
  5. Don't over-optimize for 3x DPR — 2x is sufficient for most content images.
  6. Set and enforce image performance budgets to maintain quality over time.
  7. Always include width, height, and appropriate loading attributes on every image.

The implementation effort is modest, and the payoff — faster load times, lower bandwidth consumption, and better Core Web Vitals — is substantial and measurable.