<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.
srcset candidates
against the device pixel ratio and viewport width, then downloads only the best match.
<picture> adds art direction (different crops) and format negotiation
(AVIF → WebP → JPEG). The <img> fallback ensures
universal compatibility. All selection logic runs in the browser engine —
no JavaScript required.
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.
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.
<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 -->
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 -->
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.
<!-- 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. -->
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.
<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! -->
<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.
1400 × 467 — wide landscape
800 × 500 — tighter crop
480 × 600 — portrait
<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. -->
<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.
fm=avif, fm=webp, and JPEG fallback.
Click “Check What Loaded” to see which format your browser selected.
In production, image CDNs (Cloudflare, Cloudinary, imgix) auto-convert on the fly
via the Accept header.
<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 | 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.
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.
Lossy compression for PNG and JPEG. Reduces file size dramatically with minimal visual loss. Batch processing via web or API.
tinypng.comAdvanced image optimization with lossy and lossless modes. Supports bulk uploads and integrates with cloud storage.
kraken.ioGoogle’s open-source tool. Runs entirely in-browser. Converts between AVIF, WebP, JPEG XL, and JPEG with live preview.
squoosh.appAccept header
and serve the most compressed version automatically — often eliminating
the need for manual <picture> format switching entirely.
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. -->
loading="lazy" with
fetchpriority="high" on the same image.
lazy suppresses the fetch until scroll visibility;
fetchpriority="high" tries to fetch immediately.
They conflict — the lazy constraint wins and delays the priority hint.
<!-- 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">
<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 -->
<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 -->