ausi / respimagelint

Linter for Responsive Images - https://ausi.github.io/respimagelint/
MIT License
777 stars 29 forks source link

The sizes attribute has to match the width of the image #30

Open robots4life opened 6 years ago

robots4life commented 6 years ago

Since I learned about your linter here https://beta.observablehq.com/@eeeps/w-descriptors-and-sizes-under-the-hood a few days ago I am trying to get things right.

I am confused why I get above message. Following code.

<img
    src="image-320x320.jpg"
    alt="image description"
    sizes="
        (max-width: 240px) calc(100vw - 2rem),
        (max-width: 320px) calc(100vw - 2rem),
        (max-width: 425px) calc(100vw - 2rem),
        (max-width: 560px) calc(100vw - 2rem),
        (max-width: 640px) calc(100vw - 2rem),
        (max-width: 768px) 239px,
        (max-width: 1024px) 331px,
        (max-width: 1200px) 394px,
        (max-width: 1400px) 466px,
        (max-width: 1600px) 538px,
        538px
        "
    srcset="
        image-180x180.jpg 180w,
        image-240x240.jpg 240w,
        image-320x320.jpg 320w,
        image-425x425.jpg 425w,
        image-525x525.jpg 525w,
        image-640x640.jpg 640w,
        image-768x768.jpg 768w,
        image-800x800.jpg 800w,
        image-900x900.jpg 900w,
        image-960x960.jpg 960w,
        image-1024x1024.jpg 1024w,
        image-1140x1140.jpg 1140w,
        image-1280x1280.jpg 1280w,
        image-1600x1600.jpg 1600w,
        image-1920x1920.jpg 1920w,
        image-2048x2048.jpg 2048w,
        image-2560x2560.jpg 2560w,
        image-3200x3200.jpg 3200w
        ">

I checked the page with https://html5.validator.nu/ and it has no errors, I only formatted the img code here this way to be able to better read it. The image urls are also all working. The size of the images is exactly as shown, the w descriptor value is also 100% correct.

Now for the layout. From 0px up to 640px viewport width it is a single column layout, meaning the image stretches across the available viewport width with 1rem padding on either side. So 100vw - 2rem is the space the image can take between 0px and 640px.

Above 640px the layout turns into a 2 column layout. The total size of both columns is 1600px hard limit. The left column is 36% wide the right column is 64% wide.

For the part of the layout where there is only a single column, 0px to 640px, I have come up with the above media conditions and length for the sizes attribute. I am using max-width here as I believe it is good to start with a small viewport and go up to the large one instead of the other way round (mobile-first). https://cloudfour.com/thinks/responsive-images-101-part-5-sizes/

For me this means: (max-width: 240px) calc(100vw - 2rem), ==> from 0px to 240px viewport width the image is 100% of the viewport width minus 2rem.

(max-width: 320px) calc(100vw - 2rem), ==> from 241px to 320px viewport width the image is 100% of the viewport width minus 2rem.

(max-width: 425px) calc(100vw - 2rem), ==> from 321px to 425px viewport width the image is 100% of the viewport width minus 2rem.

(max-width: 560px) calc(100vw - 2rem), ==> from 426px to 560px viewport width the image is 100% of the viewport width minus 2rem.

(max-width: 640px) calc(100vw - 2rem), ==> from 561px to 640px viewport width the image is 100% of the viewport width minus 2rem.

Once the viewport is over 640px I have carefully measured the image's width at the specified media-conditions and rounded it to the next full pixel. This left me with following sizes: (max-width: 768px) 239px, ==> from 641 to 768px viewport width the image's maximum size at 768px viewport width is 239px.

(max-width: 1024px) 331px, ==> from 769 to 1024px viewport width the image's maximum size at 1024px viewport width is 331px.

(max-width: 1200px) 394px, ==> from 1025px to 1200px viewport width the image's maximum size at 1200px viewport width is 394px.

(max-width: 1400px) 466px, ==> from 1201px to 1400px viewport width the image's maximum size at 1400px viewport width is 466px.

(max-width: 1600px) 538px, ==> from 1401px to 1600px viewport width the image's maximum size at 1400px viewport width is 538px.

The last media condition is 538px, so anything over 1600px viewport width should be served an image that fits the length of 538px.

Now comes, when I carefully reload the page while cache is disabled and check the network tab in Chrome dev tools I can perfectly see the right image size downloading and the image displays crisp and clean on all values between 200px and anything over 1600px viewport width.

Then I toggle the device toolbar and choose desktop mode (it does not work in mobile mode for some reason, but I do have the viewport meta tag in place, valid HTML5 document, must be something with Chrome dev tools) and carefully resize the page from small size to large and again I can see how just the right image sizes are downloaded for the specific viewport width.

Now I think I must have either completely misunderstood the use of sizes or something else is the case. Would you care to enlighten me please where my thought process has gone haywire? I mean the network tab shows me the right sources and Chrome audit with Lighthouse shows me everything is fine getting 100% and no "Properly size images: ..." warning comes up.

What the linter suggest to me to use for sizes is: sizes="(min-width: 1700px) 544px, (min-width: 640px) calc(33.27vw - 15px), calc(100vw - 32px)" If I use that the test passes, however why do I need to use min-width instead of max-width and why are there so few sizes specified? What would pass this test if I wanted to use max-width sizes?

The entire message I get back is following:

The sizes attribute has to match the width of the image

The size of the image doesn’t match the sizes attribute (max-width: 240px) calc(100vw - 2rem), (max-width: 320px) calc(100vw - 2rem), (max-width: 425px) calc(100vw - 2rem), (max-width: 560px) calc(100vw - 2rem), (max-width: 640px) calc(100vw - 2rem), (max-width: 768px) 239px, (max-width: 1024px) 331px, (max-width: 1200px) 394px, (max-width: 1400px) 466px, (max-width: 1600px) 538px, 538px.

At a viewport of 1440x810 the image was 486 pixels wide instead of the specified 538 (-10% difference).

The affected viewports are 640x360-660x880, 780x439-920x1227, 1040x585-1080x1440, 1220x686-1260x1680, 1420x799-1460x1947.

Try using sizes="(min-width: 1700px) 544px, (min-width: 640px) calc(33.27vw - 15px), calc(100vw - 32px)" instead.

If you have any info for me that would be really great, thank you for any time you can put into a reply.

For reference, in the past days I have read: https://cloudfour.com/thinks/responsive-images-101-definitions/ all 10 parts https://dev.opera.com/articles/responsive-images/ https://beta.observablehq.com/@eeeps/w-descriptors-and-sizes-under-the-hood https://ericportis.com/posts/2014/srcset-sizes/ https://jakearchibald.com/2015/anatomy-of-responsive-images/ https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img

ausi commented 6 years ago

Do you have a link to the affected webpage? This would make it much easier to debug.

robots4life commented 6 years ago

Thank you heaps for your reply. I will make a copy and send you a link to the page.

ausi commented 6 years ago

There are two possible reasons for this message from the linter: Either there is a bug in the linter or your sizes attribute is more than 6% different from the “real” size of the image.

Let’s take a closer look at the error message “At a viewport of 1440x810 the image was 486 pixels wide instead of the specified 538 (-10% difference).”:

  1. We can see that for a viewport width of 1440 the linter detects a specified width of 538. This seems to be detected correctly according to your source code from above. The condition (max-width: 1600px) applies in this case and sets the size to 538px.
  2. The linter detected that at this viewport (1440) the images is only 486 pixels wide. Without a link to the affected page I cannot verify that, but you can test this yourself by resizing the browser to 1440 pixels and checking how wide the image is.
  3. If the image is indeed 486 pixel wide, the linter is correct because there is a significant gap between the specified and the real size (538 vs 486).

why are there so few sizes specified?

For most layouts you don’t need a large list in the sizes attribute. Your source code for example contains some redundant information that can be removed without changing the result. The first five lines (max-width: 240px) calc(100vw - 2rem), …, (max-width: 640px) calc(100vw - 2rem) can be combined to the single line (max-width: 640px) calc(100vw - 2rem), without changing it’s meaning.

What would pass this test if I wanted to use max-width sizes?

Your whole sizes attribute can probably be rewritten to something like this:

(max-width: 640px) calc(100vw - 2rem),
(max-width: 1600px) calc(36vw - 38px),
538px

This translates to:

  1. If the viewport is 640 or less pixels wide the image width is 100% of the viewport minus 2rem (32px). Examples:
    • Viewport 240, Image 208. (240 - 32)
    • Viewport 425, Image 393. (425 - 32)
    • Viewport 640, Image 608. (640 - 32)
  2. Otherwise if the viewport is 1600 or less pixels wide the image width is 36% of the viewport minus 38px. Examples:
    • Viewport 641, Image 193. (614 * 0.36 - 38)
    • Viewport 1024, Image 331. (1024 * 0.36 - 38)
    • Viewport 1600, Image 538. (1600 * 0.36 - 38)
  3. Otherwise if the viewport is larger than 1600 pixels the image width is exactly 538px. Examples:
    • Viewport 1601, Image 538.
    • Viewport 1800, Image 538.
    • Viewport 9999, Image 538.
robots4life commented 6 years ago

Dear @ausi tremendously helpful, thank you heaps! The affected page is from a CMS but I am preparing a local identical copy and will send you the link today so you can have a look to verify yourself. Super glad you take the time to explain this in detail to me.

robots4life commented 6 years ago

Here is the repo I just made. https://github.com/robots4life/respimagelint-debug-help And here is the live page identical to what I have in the project I am working on. https://robots4life.github.io/respimagelint-debug-help/index.html Hope this helps you see what I am trying to do.

robots4life commented 6 years ago

OK, I have gone over your reply a couple of times. Before I ask you some points I have about the error message and your explanation thereof could you kindly confirm or correct these two points concerning your examples?

In the Examples section, 2nd example, you write

Viewport 641, Image 193. (614 * 0.36 - 38)

Were you meant to write Viewport 641, Image 193. (641* 0.36 - 38) instead? (641 vs 614) I understand that 1rem is 16px so 2rem are 32px, so where do the 38px come from?

Please take your time to look at the page I provided and check the values, there is no rush and be sure I am super happy to be able to learn and then get this right with your kind help.

ausi commented 6 years ago

In the Examples section, 2nd example, you write

Viewport 641, Image 193. (614 * 0.36 - 38)

Were you meant to write Viewport 641, Image 193. (641* 0.36 - 38) instead? (641 vs 614)

Yes, it’s a typo, should have been (641 * 0.36 - 38).

I understand that 1rem is 16px so 2rem are 32px, so where do the 38px come from?

The 38px was a guess based on your measured image sizes. Now, looking at your example, the correct sizes attribute is:

(max-width: 639px) calc(100vw - 32px),
(max-width: 1600px) calc(36vw - 32px),
544px

Note that it has to be (max-width: 639px) instead of (max-width: 640px). If you switch from min-width to max-width conditions you have to decrease by one pixel to get the same result. I usually use the same condition style (always min or always max) in the CSS media queries and in the sizes attribute to prevent such issues. The sizes attribute for your example can also be written with min-width conditions:

(min-width: 1600px) 544px,
(min-width: 640px) calc(36vw - 32px),
calc(100vw - 32px)
robots4life commented 6 years ago

Regarding being able to leave out the first few sizes attribute media conditions I understand you now. Regardless of if the viewport is any size under max-width 639px it will always be 100% of the viewport width minus 32px from two times 1rem padding. Hence these rules can go into one rule and the browser will automatically, given this media condition, pick the right source in the srcset attribute.

Regarding the typo and 38px, thank you for confirming, the 38px was good guess in that case.

Regarding the decreasing of 1px when doing max-width I also get you. If I would have max-width 640 as media condition in the sizes attribute as well as min-width 640px as media query in the stylesheet things would be off as at 640px min-width per media query in the stylesheet the layout changes but the sizes attribute would still have 1px room for showing the previous layout, that is the much wider image, not good.

respimagelint-debug-help-001

respimagelint-debug-help-002

A question regarding the use of min-width and max-width in the media condition for the sizes attribute. Do you think there also something like a mobile-first approach exists? i.e. is having the smallest possible value that would fit the media condition faster ? Would the browser select it faster, since it displays the first fitting image per those linked posts above and per spec and just skip checking the rest, similar to having a mobile-first stylesheet?

Or do you think does the browser in any way create an array of all the media condition in the sizes attribute, flags the one that fits and only after it has gone through all the media conditions, will it set the image from the srcset that fits the media condition best? We are possibly talking about ms here but still if you have a page with tons of images on it this can/could pile up, do you know anything about this process? What do you suggest to use, max-width or mix-width media conditions?

Regarding the 544px I got tricked into believing that the image is only ever 538px wide. This is what I saw.

respimagelint-debug-help-003

But in fact it was the vertical scrollbar that was still in place in the layout because the original page is longer and has a vertical scrollbar. If I extend the responsive view to not have the vertical scrollbar the viewport width is larger and then the image at 1600px vw is also 544px wide. See this happening below.

respimagelint-debug-help-004

Also, since there is no media query at 1600px but only a style rule that the layout has a max-width of 1600px the last sizes attribute media condition can very well be at max-width 1600px, since that is also the max-width that is being declared in the stylesheet, albeit not as a media query. This also tricked me up quite a bit.

Needless to say, both

(max-width: 639px) calc(100vw - 32px), (max-width: 1600px) calc(36vw - 32px), 544px

and

(min-width: 1600px) 544px, (min-width: 640px) calc(36vw - 32px), calc(100vw - 32px)

sizes pass the test and now I also understand why.

The last observation I could make is that indeed Chrome will load a different image depending on if the responsive view is in Desktop or Mobile mode. This I found quite interesting and perhaps of interest for other devs trying to get their image sizes right. So if you check your image sizes in Chrome responsive mode do use Desktop mode.

respimagelint-debug-help-005

From what I understand the linter uses a headless browser, phantom.js, is that right? So for the linter it will not matter if the page has a vertical scrollbar or not, am I right? It will evaluate the correct image size in the given layout without scrollbars, correct?

Unrelated: Concerning Phantom.js, that I also love to use to extract critical css from the page I just learned this https://github.com/ariya/phantomjs/issues/15344 Given that, are there any plans to use something else in case the linter gets an update?

Bottom line, Martin, thank you tremendously for your kind help and very detailed explanation. I think you can see from my reply that I understand this much better now, thanks to you and your infamous responsive image linter. Be sure I shall be using this tool in my dev workflow since it really helped me getting things right. Solid gear, thx!

ausi commented 6 years ago

… do you think does the browser in any way create an array of all the media condition in the sizes attribute, flags the one that fits and only after it has gone through all the media conditions, will it set the image from the srcset that fits the media condition best? We are possibly talking about ms here but still if you have a page with tons of images on it this can/could pile up, do you know anything about this process? What do you suggest to use, max-width or mix-width media conditions?

I don’t think the performance is different between min- and max-width conditions. CSS media conditions should be highly optimized in browsers. I usually use the same condition style for the sizes attribute as the stylesheet on the page uses.

From what I understand the linter uses a headless browser, phantom.js, is that right?

No, the linter runs directly in your browser. Only the automated test suite uses phantom.js to verify that the linter works correctly.

So for the linter it will not matter if the page has a vertical scrollbar or not, am I right? It will evaluate the correct image size in the given layout without scrollbars, correct?

The linter creates an iframe for testing the page in different viewports. The scrollbars in the iframe are disabled to get more consistent values across different browsers. You can take a look at the source code of the linter to see how it works in more detail: https://github.com/ausi/respimagelint/blob/master/src

Bottom line, Martin, thank you tremendously for your kind help and very detailed explanation.

You’re welcome :)
Thanks also to my Patreons which help me funding my open source work.