Implementing Responsive Images with srcset and Sirv
A hands-on tutorial covering srcset, sizes, picture elements, pixel density descriptors, and CDN-powered responsive images for optimal performance.
What You’ll Learn
By the end of this tutorial, you’ll know how to:
- Use
srcsetwith width descriptors (w) for resolution switching - Use
srcsetwith pixel density descriptors (x) for retina images - Write
sizesattributes that match your CSS layout - Apply art direction with
<picture>for different crops at different breakpoints - Combine responsive images with an image CDN to avoid maintaining multiple files
- Test and debug responsive images in the browser
- Handle common pitfalls (missing sizes, CLS, lazy loading interactions)
Why Should You Care About Responsive Images?
A product image that looks sharp on a 27” iMac is wildly oversized for a phone screen. Without responsive images, every visitor downloads the largest version. That’s a huge waste of bandwidth on mobile, and it murders your load times.
The numbers are stark:
- The median webpage serves over 1 MB of images (HTTP Archive, January 2026)
- A properly sized image can be 50-80% smaller than a one-size-fits-all approach
- LCP (Largest Contentful Paint) improves directly when the hero image is right-sized: smaller file = faster download = lower LCP
Responsive images fix this by giving the browser a menu of image sources and letting it pick the best one for the current screen.
The Two Approaches
HTML gives you two ways to do responsive images:
srcset+sizes: You provide candidates, the browser decides. It picks based on viewport width and pixel density.<picture>element: You control exactly which image loads at each breakpoint. This is for “art direction,” where you need different crops or aspect ratios.
Most use cases need approach #1. Only reach for approach #2 when the image itself needs to change (like a wide panorama on desktop but a tight portrait crop on mobile).
Step 1: srcset with Width Descriptors
The w descriptor tells the browser the intrinsic width of each candidate:
<img
src="https://demo.sirv.com/product.jpg?w=800"
srcset="
https://demo.sirv.com/product.jpg?w=400 400w,
https://demo.sirv.com/product.jpg?w=800 800w,
https://demo.sirv.com/product.jpg?w=1200 1200w,
https://demo.sirv.com/product.jpg?w=1600 1600w
"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt="Responsive product photo"
width="800"
height="600"
/>
How the browser decides
The browser reads sizes to figure out how wide the image will render, then picks the smallest srcset candidate that’s big enough, factoring in the device pixel ratio (DPR).
Take a 375px-wide iPhone (DPR 3):
sizessays100vw, so the image renders at 375px- At 3x DPR, the browser wants at least 375 x 3 = 1125 pixels
- It grabs the
1200wcandidate (the smallest one that clears 1125)
Now a 1440px desktop (DPR 1):
sizessays33vw, so the image renders at roughly 475px- At 1x DPR, 475 pixels is all it needs
- It grabs the
800wcandidate
Choosing your width values
Don’t try to match specific devices. Base your widths on your actual layout instead:
- Check your image’s rendered width at common viewport sizes
- Create candidates at roughly 1.5-2x intervals
- 4-6 candidates typically covers the full range
A practical set for a product grid: 400w, 600w, 800w, 1200w, 1600w.
Step 2: Writing the sizes Attribute
sizes is a comma-separated list of media conditions paired with lengths. The browser uses the first match:
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
This reads as:
- On viewports up to 640px: image takes full width (100vw)
- On viewports up to 1024px: image takes half width (50vw)
- Otherwise: image takes one-third width (33vw)
Match sizes to your CSS
Your sizes attribute must reflect your actual CSS layout. If your CSS says the image container is calc(50% - 2rem) on desktop, your sizes should approximate that:
sizes="(min-width: 1024px) calc(50vw - 2rem), 100vw"
What happens without sizes?
Skip sizes and the browser defaults to 100vw. It assumes the image fills the entire viewport. On a 2x retina screen, that means it downloads the biggest available image even if the image only takes up 25% of the screen. Always include sizes.
Step 3: srcset with Pixel Density Descriptors
For fixed-width images (logos, avatars, icons), use density descriptors instead of width descriptors:
<img
src="https://demo.sirv.com/logo.png?w=200"
srcset="
https://demo.sirv.com/logo.png?w=200 1x,
https://demo.sirv.com/logo.png?w=400 2x,
https://demo.sirv.com/logo.png?w=600 3x
"
alt="Company logo"
width="200"
height="50"
/>
The browser picks based on the device’s pixel ratio:
- Standard display (1x DPR): downloads the 200px version
- Retina (2x DPR): downloads the 400px version
- iPhone Pro (3x DPR): downloads the 600px version
When to use which:
wdescriptors: Fluid images that change size with the viewport (product grids, heroes, content images)xdescriptors: Fixed-size images that stay the same regardless of viewport (logos, avatars, icons, thumbnails)
Step 4: Art Direction with picture
Need different crops at different breakpoints, not just different sizes? That’s what <picture> is for:
<picture>
<!-- Mobile: tight crop on the product -->
<source
media="(max-width: 640px)"
srcset="https://demo.sirv.com/product.jpg?w=640&h=640&crop.type=face"
/>
<!-- Tablet: medium crop -->
<source
media="(max-width: 1024px)"
srcset="https://demo.sirv.com/product.jpg?w=1024&h=600&scale.option=fill"
/>
<!-- Desktop: wide hero shot -->
<img
src="https://demo.sirv.com/product.jpg?w=1400&h=500&scale.option=fill"
alt="Product hero image"
width="1400"
height="500"
/>
</picture>
With Sirv’s dynamic imaging, you don’t need to manually create these crops. Just adjust the URL parameters for each breakpoint. The crop.type=face parameter auto-detects faces for smart cropping.
Combining picture with srcset
Want maximum control? Combine <picture> for art direction with srcset for resolution switching within each breakpoint:
<picture>
<source
media="(max-width: 640px)"
srcset="
https://demo.sirv.com/product.jpg?w=400&h=400&scale.option=fill 400w,
https://demo.sirv.com/product.jpg?w=800&h=800&scale.option=fill 800w
"
sizes="100vw"
/>
<source
media="(max-width: 1024px)"
srcset="
https://demo.sirv.com/product.jpg?w=600&h=400&scale.option=fill 600w,
https://demo.sirv.com/product.jpg?w=1200&h=800&scale.option=fill 1200w
"
sizes="50vw"
/>
<img
src="https://demo.sirv.com/product.jpg?w=800"
srcset="
https://demo.sirv.com/product.jpg?w=600 600w,
https://demo.sirv.com/product.jpg?w=1200 1200w,
https://demo.sirv.com/product.jpg?w=1800 1800w
"
sizes="33vw"
alt="Product"
width="800"
height="600"
/>
</picture>
Step 5: Preventing Layout Shift (CLS)
Responsive images cause Cumulative Layout Shift (CLS) when the browser doesn’t know the aspect ratio before the image loads. The fix is simple. Always include width and height attributes:
<img
src="photo.jpg?w=800"
srcset="photo.jpg?w=400 400w, photo.jpg?w=800 800w"
sizes="(max-width: 640px) 100vw, 50vw"
alt="Product"
width="800"
height="600"
/>
Modern browsers use the width/height ratio to reserve space before anything loads. Pair it with this CSS:
img {
max-width: 100%;
height: auto;
}
That keeps images fluid while maintaining aspect ratio and preventing layout shifts.
When the aspect ratio changes between breakpoints (art direction), use the CSS aspect-ratio property:
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
@media (max-width: 640px) {
.hero-image {
aspect-ratio: 1 / 1;
}
}
Step 6: Lazy Loading Gotchas
Be careful combining responsive images with lazy loading:
<!-- GOOD: Lazy load below-the-fold images -->
<img
src="product.jpg?w=800"
srcset="product.jpg?w=400 400w, product.jpg?w=800 800w"
sizes="50vw"
loading="lazy"
alt="Product below the fold"
width="800"
height="600"
/>
<!-- GOOD: Don't lazy load the hero/LCP image -->
<img
src="hero.jpg?w=1200"
srcset="hero.jpg?w=800 800w, hero.jpg?w=1200 1200w, hero.jpg?w=1600 1600w"
sizes="100vw"
loading="eager"
fetchpriority="high"
alt="Hero image"
width="1200"
height="600"
/>
Your LCP image (usually the hero) needs loading="eager" (or just omit the attribute, since eager is the default) plus fetchpriority="high" to tell the browser to prioritize it.
Also consider preloading the hero with a <link> tag in the <head>:
<link
rel="preload"
as="image"
href="hero.jpg?w=1200"
imagesrcset="hero.jpg?w=800 800w, hero.jpg?w=1200 1200w, hero.jpg?w=1600 1600w"
imagesizes="100vw"
/>
Note: imagesrcset and imagesizes are the preload equivalents of srcset and sizes.
Step 7: The CDN Advantage
Without a CDN, responsive images means maintaining multiple files per image. Resized versions at each breakpoint, in each format. For a product catalog of 1,000 images with 5 size variants and 3 formats, that’s 15,000 files. Nobody wants to manage that.
With an image CDN like Sirv, you upload one master image and generate variants through URL parameters:
/product.jpg?w=400 → 400px wide
/product.jpg?w=800 → 800px wide
/product.jpg?w=1200 → 1200px wide
Format conversion happens automatically. Sirv checks the browser’s Accept header and serves AVIF, WebP, or JPEG accordingly. No <picture> element needed for format switching.
Sirv’s automatic approach
Want the simplest possible implementation? Sirv’s JavaScript handles everything:
<img class="Sirv" data-src="https://your-account.sirv.com/product.jpg" alt="Product">
<script src="https://scripts.sirv.com/sirvjs/v3/sirv.js"></script>
This detects the image’s rendered size, requests the right dimensions from the CDN, serves the best format, adds lazy loading, and handles retina displays. All without writing srcset or sizes.
The trade-off: you give up fine-grained control over sizes and breakpoints in exchange for zero-config responsive images.
Step 8: Testing and Debugging
Chrome DevTools
- Open DevTools and go to the Network panel
- Filter by “Img” type
- Resize the browser window and reload
- Check which srcset candidate was downloaded (look at the “Size” column)
- Toggle device emulation (phone, tablet) to test different DPRs
Verify the right image loaded
In the Network panel, hover over the image URL to see the dimensions. Compare against your srcset candidates to confirm the browser chose correctly.
Common debugging issues
| Problem | Likely Cause | Fix |
|---|---|---|
| Always loads the largest image | Missing sizes attribute (defaults to 100vw) | Add sizes matching your CSS layout |
| Image looks blurry | srcset candidates too small for the DPR | Add larger width candidates |
| CLS in Lighthouse | Missing width/height on <img> | Add explicit width and height attributes |
| srcset seems ignored | src URL matches a srcset URL | Ensure src is a distinct fallback |
| Wrong candidate chosen | sizes doesn’t match actual CSS | Inspect rendered width and adjust sizes |
Lighthouse audit
Run a Lighthouse performance audit and look for:
- “Properly size images”: flags images larger than their rendered size
- “Serve images in modern formats”: flags JPEG/PNG that could be WebP/AVIF
- “Image elements do not have explicit width and height”: CLS warning
Quick Reference Checklist
- Add
srcsetwith 4-6 width candidates matching your layout - Include
sizesthat reflects your CSS (don’t default to 100vw) - Set
widthandheighton all<img>tags for CLS prevention - Use
loading="lazy"on below-the-fold images only - Add
fetchpriority="high"to your LCP image - Preload the hero image in
<head>for fastest LCP - Use
<picture>only for art direction (different crops), not just format switching - Test across viewports in DevTools to verify correct candidate selection
Related Resources
The Image Performance Audit: A Step-by-Step Workflow for 2026
A practical, tool-driven workflow for auditing and fixing image performance issues. Covers Lighthouse, WebPageTest, Chrome DevTools, and CDN-based automation.
Getting Started with Sirv CDN: A Complete Guide
Set up Sirv CDN for your website step by step. Upload, optimize, and deliver images through a global CDN with dynamic imaging via URL parameters.