srcset vs media queries : Which Should You Use & Why?

To solve the problem of “responsive images” (the correct sized image served to every device based on resolution) you might feel like reaching for “media queries”. After all, you already know how to use them well. Nobody wants to learn a new thing! Surely I did not. But, that would NOT be a good idea.

“srcset” is the tool built for this job. I will share with you why in this detailed breakdown of “srcset vs media queries”. I did some testing.

The short answer: Media queries have limited support for pixel density. Because of this, images will not be properly selected on all devices & will look blurry. Also, unless you “lazy” load the images, the browser will load all sizes. (Even if they are set to display:none) This defeats the purpose. srcset does not have these problems.

Video Version Of Article

The Problem We Need To Solve

Nowadays, most pages are responsive. We change the layout of the page, based on the device screen. Take the example below:

On the desktop, the image takes up half the screen. On the mobile, it takes up the complete screen width.

So, the question becomes: What should be the size of the image?

  • If you size the image based on the mobile screen, it might be too small for the desktop.
  • If you size the image based on the desktop screen, it might be too big for some mobiles.

Getting the wrong size is bad because:

  • Either the size is too small, and the image looks blurry.
  • Or the size is too big, and the poor user is downloading a massive image that he does not need.

And if all of this was not problematic enough, the resolution of the devices people use is varied. Below is the current status from StatCounter.

Worst of all, Google is not happy that you are giving your users a bad experience. So, you will take a hit on the SEO front too.

Basically, this is not a problem to take lightly. So, how do we give the user the right sized image every time?

Let’s Attempt To Solve The Problem With Media Queries (Spoiler, it’s not going to work well)

The plan: Let’s create many versions of our image with different sizes. Then we will show and hide the images using “display:none” and “display:block” with CSS media queries.

First, we need to decide on the sizes. For this, we will take inspiration from the Bootstrap 5 media queries.

screenshot of the boostrap 5 media queries sizes

So, based on this, we are going to size our images in the following dimensions (width): 576px, 768px, 992px, 600px, 700px, 960px

I have made a demo that you can look at here to see this all in action.

Here you can see the IMG tags of all the sizes. To keep things simple and clear, I am using the placeholder.jp (image placeholder) service.

<img src="https://placehold.jp/576x576.png" class="col-6-xs-img" />
<img src="https://placehold.jp/768x768.png" class="col-6-sm-img" />
<img src="https://placehold.jp/992x992.png" class="col-6-md-img" />
<img src="https://placehold.jp/600x600.png" class="col-6-lg-img" />
<img src="https://placehold.jp/700x700.png" class="col-6-xl-img" />
<img src="https://placehold.jp/960x960.png" class="col-6-xxl-img" />

As you can see, each of the images have a class like: “col-6-md-img”

I have given the classes Boostrappy names. I have also created a lot of media query rules to show and hide the images based on screen resolution in pixels.

/* Small devices (landscape phones, 576px and up) */
@media (min-width: 576px) {

   .col-6-xs-img {
       display: none;
   }

   .col-6-sm-img {
       display: block;
   }

}

/* Medium devices (tablets, 768px and up) */
@media (min-width: 768px) {

   .col-6-sm-img {
       display: none;
   }

   .col-6-md-img {
       display: block;
   }

}

/* Large devices (desktops, 992px and up) */
@media (min-width: 992px) {

   .col-6-md-img {
       display: none;
   }

   .col-6-lg-img {
       display: block;
   }

}

/* X-Large devices (large desktops, 1200px and up) */
@media (min-width: 1200px) {

   .col-6-lg-img {
       display: none;
   }

   .col-6-xl-img {
       display: block;
   }

}


/* XX-Large devices (larger desktops, 1400px and up) */
@media (min-width: 1400px) {

   .col-6-xl-img {
       display: none;
   }

   .col-6-xxl-img {
       display: block;
   }

}

Okay, so after all this effort, things still don’t really work well. Let’s look at why. There are two points of failure.

Failure 1: Even Though Only 1 Image Is “display:block” At A Time, Browser Still Loads Them All

screenshot of all the images being loaded in the network tab

This is silly. This defeats the purpose. We started by trying to load less for the user. We ended up loading 5 times more.

But, we can fix this. We can add another parameter to the image tag loading=’lazy’:

<img src="https://placehold.jp/576x576.png" class="col-6-xs-img" loading="lazy" />
<img src="https://placehold.jp/768x768.png" class="col-6-sm-img" loading="lazy" />
<img src="https://placehold.jp/992x992.png" class="col-6-md-img" loading="lazy" />
<img src="https://placehold.jp/600x600.png" class="col-6-lg-img" loading="lazy" />
<img src="https://placehold.jp/700x700.png" class="col-6-xl-img" loading="lazy" />
<img src="https://placehold.jp/960x960.png" class="col-6-xxl-img" loading="lazy" />

Once, we add this, we see that Chrome only loads the image that’s displayed and hides the rest.

chrome network tab screenshot with only the image needed being loaded

And even though this works, it’s not a great solution. Because CanIUse, says that this lazy loading of images has poor support.

loading lazy attr support caniuse

But, there is one more issue with this approach..

Failure 2: These Media Queries Do No Factor In “Retina” Displays & Pixel Ratio – So Images Are Blurry

Consider this situation. The image being picked on the iPhone 12 is 576px width. This is very small. If we serve this image, it will look blurry.

screenshot of iphone12 resolution chrome simulation

Because the real resolution of the iPhone12 is 1170px by 2532px.

iphone12 resolution

So, the correct non-blurry image should have been atleast 1170px width.

Why does this happen? Because the resolution of the phone is not 390px  by 844px as Chrome is saying in the above image. It’s actually 2532px by 1170px. And our CSS media queries do not account for it.

You have probably heard that the iPhone has a “retina display”. All of the confusion above is due to that.

If you are confused about what is going on, what “Retina Displays” are and why this issue exists, you need to listen to Steve Jobs himself explain it all to you:

Basically according to CSS Media Queries the screen width of an iPhone 12 is: 390px but the device has a “devicePixelRatio” (number of real pixels per vitual pixel) of 3 and so the real width is 1170px.

390 x 3 = 1170

You can run the following javascript to check the device pixel ratio.

query device pixel ratio using javascript

I have gone into much more detail about all this in my article: srcset not working? 😩 Getting Wrong Images? Let’s Find Out Why!

However, we can deal with all these problems also using media queries.

/* Exact resolution */
@media (-webkit-device-pixel-ratio: 1) {

}

/* Minimum resolution */
@media (-webkit-min-device-pixel-ratio: 1.1) {

}

/* Maximum resolution */
@media (-webkit-max-device-pixel-ratio: 3) {

}

But, there are 2 reasons why this is not a good idea:

  • The support for this is not really there. See what CanIUse has to say.
  • Your code is going to get even more complex. And why bother when there is a much better way.

canIuse screenshot of device pixel ratio showing poor support

Okay, with all these challenges, I think we can agree, that we need a better solution.

Let’s Solve The Problem With srcset

This is what the code looks like (notice I have added many more sizes since we need them for the higher iPhone12 resolution):

<img src="https://placehold.jp/576x576.png" 
            srcset="https://placehold.jp/576x576.png 576w,
                        https://placehold.jp/768x768.png 768w,
                        https://placehold.jp/992x992.png 992w,
                        https://placehold.jp/600x600.png 600w,
                        https://placehold.jp/700x700.png 700w,
                        https://placehold.jp/960x960.png 960w,
                        https://placehold.jp/1200x1200.png 1200w,
                        https://placehold.jp/1500x1500.png 1500w,
                        https://placehold.jp/1700x1700.png 1700w,
                        https://placehold.jp/1920x1920.png 1920w" 
            sizes="(min-width: 992px) 50vw, 100vw"
/>

As a crash course on srcset, let me break up the code and explain it all:

<img src="

It’s a normal IMG tag that you know from your very first day of HTML.

srcset="https://placehold.jp/576x576.png 576w,
            https://placehold.jp/768x768.png 768w,
            https://placehold.jp/992x992.png 992w,
            https://placehold.jp/600x600.png 600w,
            https://placehold.jp/700x700.png 700w,
            https://placehold.jp/960x960.png 960w,
            https://placehold.jp/1200x1200.png 1200w,
            https://placehold.jp/1500x1500.png 1500w,
            https://placehold.jp/1700x1700.png 1700w,
            https://placehold.jp/1920x1920.png 1920w"

There is a new attribute called: “srcset” which is a “set of sources”.

Each source has 2 parts: URL & Size in a unit “w”

https://placehold.jp/768x768.png 768w

The “w” unit might seem weird at first. It’s just used to tell the browser: “width of the image in px”. Height does not matter.

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

There is also a “sizes” attribute. It tells the browser, that depending on the screen size, what part of the screen or “vw” or “viewport width” does the image take up.

(min-width: 992px) 50vw, 100vw

In the above case, it says: for a min width of 992px and above (a desktop) it takes up 50% or 50vw of the screen.

And anything that does not match a previous condition, it takes up 100 viewport width.

Let’s test out the above code (you can play with it here). If we look at the same situation of the iPhone12 which image does the browser choose?

demo of srcset on the chrome simulator, showing that the right image is picked

So, just to refresh, the ACTUAL width of the iPhone is 1170px and Chrome is perfectly picking the image that’s slightly bigger: 1200px in size. Yay! The browser is doing all the heavy lifting and picking the correct image.

Not only that, only one image is being loaded at a time based on what is needed:

screenshot of network tab of chrome with only one image being loaded

And the support of srcset is much higher than all the options we talked about above.

better support for srcset vs media queries as shown by caniuse

So, it seems that hands down srcset is the method of choice.

What Sizes Should You Make?

Okay. So, you are convinced that srcset is the better way to go. Now, you need to implement it. A good way to decide on the number of image sizes is to follow what NextJS (a js framework that has srcset built in) is doing. By default, these are the size they generate: 640, 750, 828, 1080, 1200, 1920, 2048, 3840

nextjs screenshot of the image sizes they make

Yes, that is a lot of sizes. I agree. It’s going to be a pain to generate all of them. So, let me share with you some tips for getting it does painlessly.

How Do You Make So Many Sizes?

You have to use an ImageCDN. Basically it’s a service, you can generate as many image sizes you want on the fly by changing the URL slightly.

For example:

To generate the above image, I have used the ImageKit.io CDN. It has very generous free plan: 20 GB bandwidth & 20 GB media storage.

Also, it supports a wide variety of platforms

all the platforms supported by imagekit.io

Conclusion

  • Don’t use media queries for responsive images.
  • It’s not the right tool for the job. Use srcset.
  • Code with media queries is complex and not needed.
  • Media query support for device pixel ratio is detection is not present.
  • Generate a lot of sizes using an ImageCDN when using srcset
  • Let the browser do all the heavy lifting.
  • I hope this settles the srcset vs media queries or you.