Responsive Images

<picture>, srcset, and sizes let the browser choose the optimal image for every device — resolution, viewport size, and format. Zero JavaScript. Bootstrap’s .img-fluid only scales with CSS; these native attributes optimize the payload.

Your device pixel ratio: — the browser uses this when choosing between 1x, 2x, 3x candidates.

DPR Device Examples Ecosystem
1x iPad 1/2, Galaxy Tab 2, iPhone 3GS, Blackberry Torch Legacy mobile, standard 1080p desktop monitors, entry-level laptops
1.5x Surface Pro 2/3/4, Surface Book, Kindle Fire HD, Galaxy S/S2 Windows convertible tablets, mid-tier Android
2x iPhone 4/5/6/7/8, iPad Air/Pro, MacBook Retina, HTC Nexus 9 Mainstream Apple ecosystem, 4K desktop monitors
3x iPhone X/11/12/13/14/15/16 Pro, iPhone Plus/Max, Galaxy S5 Premium Apple, premium Android handsets
4x Galaxy S6/S7/S8/S9/S10, Galaxy Note 10+, LG G4/G5 Ultra-high-density flagship Android

Practical targeting: Only provide 1x, 2x, 3x candidates. Fractional DPRs (1.5x, 2.75x, etc.) are handled automatically — the browser picks the next larger candidate and downsamples. Serving 4x assets yields negligible visual gains with massive file size costs.

1. The Problem — One Image for All Devices

A plain <img src="photo.jpg"> with Bootstrap’s .img-fluid scales the image visually via CSS (max-width: 100%; height: auto), but the browser still downloads the full-size file. On a 375px mobile screen, you load a 1920px image — wasted bandwidth. On a 3x Retina display with a tiny source, you get a blurry mess. No payload optimization at all.

Old Way: Single <img src>
<img
  src="photo-1920.jpg"
  class="img-fluid"
  alt="A landscape photo"
>

<!-- Problems:
  - Mobile downloads 1920px file (wasted bandwidth)
  - 1x display: correct size, but 2x/3x are blurry
  - No format optimization (JPEG only)
  - No lazy loading
  - No layout shift prevention
  - .img-fluid is CSS scaling only —
    no bandwidth saved -->
New Way: srcset + sizes
<img
  srcset="photo-480.jpg 480w,
          photo-800.jpg 800w,
          photo-1200.jpg 1200w,
          photo-1920.jpg 1920w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1200px) 50vw,
         800px"
  src="photo-800.jpg"
  alt="A landscape photo"
  class="img-fluid"
  width="1920" height="1080"
  loading="lazy"
>

<!-- Browser picks the right file automatically -->

2. Resolution Switching — x Descriptors (Fixed Size)

For fixed-size images like logos or avatars, use x descriptors. The browser picks the candidate matching the device pixel ratio. A 2x Retina display loads the 200×200 image; a 1x display loads the 100×100 image. Each file is only downloaded if needed.

Avatar (100×100)
Portrait of a smiling man
Card thumbnail (200×300, portrait)
White and black boat on lake near green mountain
Code
<!-- Square avatar -->
<img
  srcset="avatar.jpg    1x,
          avatar@2x.jpg 2x,
          avatar@3x.jpg 3x"
  src="avatar.jpg"
  alt="User avatar"
  width="100" height="100"
>

<!-- Portrait card thumbnail -->
<img
  srcset="thumb.jpg    1x,
          thumb@2x.jpg 2x,
          thumb@3x.jpg 3x"
  src="thumb.jpg"
  alt="Landscape photo"
  width="200" height="300"
>

<!-- @2x/@3x naming convention is standard.
     Use x descriptors ONLY for fixed-size images.
     Browser selects based on devicePixelRatio.
     Cannot mix x and w in the same srcset. -->

3. Resolution Switching — w Descriptors + sizes (Fluid)

For fluid images that change size with the viewport, use w descriptors to tell the browser each candidate’s intrinsic width, and sizes to declare the image’s display width at each breakpoint. The browser combines sizes with devicePixelRatio to pick the optimal file. Resize your browser window and reload to see a different candidate selected.

Aerial view of a small town on the shore of a lake
How the browser selection algorithm works
<img
  srcset="photo-480.jpg   480w,
          photo-800.jpg   800w,
          photo-1200.jpg 1200w,
          photo-1920.jpg 1920w,
          photo-2600.jpg 2600w"
  sizes="(max-width: 575.98px) calc(100vw - 24px),
         (max-width: 767.98px) 516px,
         (max-width: 991.98px) 696px,
         (max-width: 1199.98px) 936px,
         (max-width: 1399.98px) 1116px,
         1296px"
  src="photo-800.jpg"
  alt="Landscape"
  width="2600" height="1463"
  class="img-fluid"
>

<!-- sizes must match your actual CSS layout!
     These values match Bootstrap .container widths
     minus 24px padding (12px each side).

     Step 1: Evaluate sizes — find first match
     e.g. viewport = 500px → calc(100vw - 24px) = 476px

     Step 2: Multiply by devicePixelRatio
     e.g. 476px × 2 = 952px effective

     Step 3: Pick smallest candidate ≥ 952w
     → photo-1200.jpg (1200w)

     At xxl (≥1400px): 1296px × 2 = 2592px
     → photo-2600.jpg (2600w)

     If sizes is omitted, defaults to 100vw —
     always downloads the largest candidate! -->

4. Art Direction — <picture> + <source media>

Art direction goes beyond scaling: you serve different crops for different viewports. A wide landscape on desktop, a tighter crop on tablet, a tall portrait on mobile. The <source media> element uses media queries to control which image loads. Unlike srcset, this is deterministic — the browser must obey the media query. Resize the browser to see the switch.

Black Porsche 911 in a parking lot, cropped differently per viewport
Desktop (≥992px)

1400 × 467 — wide landscape

Tablet (≥576px)

800 × 500 — tighter crop

Mobile (<576px)

480 × 600 — portrait

Code
<picture>
  <source media="(min-width: 992px)"
          srcset="hero-wide.jpg">
  <source media="(min-width: 576px)"
          srcset="hero-medium.jpg">
  <img src="hero-portrait.jpg"
       alt="Hero image"
       class="img-fluid"
       width="1400" height="467">
</picture>

<!-- <picture> is invisible to screen readers.
     alt text goes on <img> only.
     The browser picks the first <source>
     whose media query matches — top to bottom.
     Unlike srcset, this is deterministic. -->

5. Format Negotiation — <picture> + <source type>

Serve the most efficient format the browser supports. List sources from most compressed (AVIF) to universal fallback (JPEG). The browser tries each <source> in order and uses the first one whose type it can decode. Browsers that support AVIF skip WebP and JPEG entirely.

Golden Gate Bridge during daytime
Production code
<picture>
  <!-- Best compression: AVIF (~50% smaller than JPEG) -->
  <source type="image/avif"
          srcset="photo-480.avif 480w,
                  photo-800.avif 800w,
                  photo-1200.avif 1200w"
          sizes="(max-width: 600px) 100vw, 800px">

  <!-- Good compression: WebP (~30% smaller than JPEG) -->
  <source type="image/webp"
          srcset="photo-480.webp 480w,
                  photo-800.webp 800w,
                  photo-1200.webp 1200w"
          sizes="(max-width: 600px) 100vw, 800px">

  <!-- Universal fallback: JPEG -->
  <img src="photo-800.jpg"
       srcset="photo-480.jpg 480w,
              photo-800.jpg 800w,
              photo-1200.jpg 1200w"
       sizes="(max-width: 600px) 100vw, 800px"
       alt="Photo"
       class="img-fluid"
       width="1200" height="675"
       loading="lazy"
       decoding="async">
</picture>

<!-- Browser evaluates <source> tags top to bottom.
     First supported type wins. -->
Format Comparison
Format Savings vs JPEG Quality Browser Support
AVIF ~50% smaller Excellent (HDR, wide gamut, alpha) Chrome 85+, Firefox 113+, Safari 16.4+, Edge 121+
WebP Recommended ~30% smaller Great quality, fast decode, small size Chrome 32+, Firefox 65+, Safari 16+, Edge 18+
JPEG XL ~60% smaller Lossless from JPEG, progressive Safari 17+ only (partial) — not baseline
JPEG Good (universal, well-optimized) Universal fallback

Practical advice: Prefer WebP as your default format — it offers excellent quality at significantly smaller file sizes than JPEG, decodes fast, and has universal browser support. Use AVIF as the top-tier option in the <picture> fallback chain for browsers that support it.

6. Optimize First, Then Serve Responsively

srcset and <picture> deliver the right size and format, but they cannot fix an unoptimized source file. Serving a 5 MB uncompressed JPEG at multiple widths just gives you five large files instead of one. Always compress your images before generating responsive variants.

TinyPNG

Lossy compression for PNG and JPEG. Reduces file size dramatically with minimal visual loss. Batch processing via web or API.

tinypng.com
Kraken.io

Advanced image optimization with lossy and lossless modes. Supports bulk uploads and integrates with cloud storage.

kraken.io
Squoosh

Google’s open-source tool. Runs entirely in-browser. Converts between AVIF, WebP, JPEG XL, and JPEG with live preview.

squoosh.app

7. Performance Attributes

Modern HTML attributes give the browser hints about priority, timing, and layout. These work on any <img> — no JavaScript needed.

loading="lazy"

Defer loading offscreen images until the user scrolls near them. Removes images from the critical rendering path.

Safari 16.4+, Chrome 77+, Firefox 121+

<img src="below-fold.jpg"
     loading="lazy"
     alt="..." >
fetchpriority="high"

Tell the browser this image is critical (e.g., LCP hero). Fetched before other images in the queue.

Chrome 101+, Firefox 132+, Safari 17.2+

<img src="hero.jpg"
     fetchpriority="high"
     alt="..." >
decoding="async"

Allow the browser to decode the image off the main thread, avoiding blocking other content from rendering.

Chrome 65+, Firefox 63+, Safari 11.1+

<img src="photo.jpg"
     decoding="async"
     alt="..." >
width + height (CLS Prevention)

Always set width and height. The browser calculates the aspect ratio before download, reserving space to prevent layout shift. Only 32% of sites do this.

<img src="photo.jpg"
     width="1920" height="1080"
     class="img-fluid"
     alt="..." >

<!-- Browser reserves 16:9 space
     before download. No layout jump. -->
Best Practice: All Attributes Combined
<!-- Hero image (above the fold) -->
<img src="hero.jpg"
     srcset="hero-480.jpg 480w, hero-800.jpg 800w, hero-1200.jpg 1200w"
     sizes="100vw"
     width="1200" height="675"
     fetchpriority="high"
     decoding="async"
     alt="Hero image"
     class="img-fluid">

<!-- Below-the-fold image -->
<img src="card.jpg"
     srcset="card-480.jpg 480w, card-800.jpg 800w"
     sizes="(max-width: 768px) 100vw, 50vw"
     width="800" height="600"
     loading="lazy"
     decoding="async"
     alt="Card image"
     class="img-fluid">

8. Code Comparison — Old Way vs New Way

Old: <img src> + .img-fluid
<img
  src="photo-1920.jpg"
  class="img-fluid"
  alt="Photo"
>

<!-- Result:
  ✗ Mobile: downloads 1920px file
  ✗ 1x screen gets correct size,
    but 2x/3x are blurry
  ✗ No format negotiation
  ✗ No lazy loading
  ✗ No layout shift prevention
  ✗ .img-fluid is CSS scaling only —
    no bandwidth saved -->
New: Native Responsive Images
<picture>
  <source type="image/avif"
    srcset="photo-480.avif 480w,
            photo-800.avif 800w,
            photo-1200.avif 1200w"
    sizes="(max-width: 600px) 100vw,
           (max-width: 1200px) 50vw,
           800px">
  <source type="image/webp"
    srcset="photo-480.webp 480w,
            photo-800.webp 800w,
            photo-1200.webp 1200w"
    sizes="(max-width: 600px) 100vw,
           (max-width: 1200px) 50vw,
           800px">
  <img
    srcset="photo-480.jpg 480w,
            photo-800.jpg 800w,
            photo-1200.jpg 1200w"
    sizes="(max-width: 600px) 100vw,
           (max-width: 1200px) 50vw,
           800px"
    src="photo-800.jpg"
    alt="Photo"
    class="img-fluid"
    width="1200" height="675"
    loading="lazy"
    decoding="async">
</picture>

<!-- Result:
  ✓ Right size for every viewport
  ✓ Best format (AVIF > WebP > JPEG)
  ✓ Lazy loaded below the fold
  ✓ No layout shift (width/height)
  ✓ Zero JavaScript -->