Search code examples
htmlcssimagecore-web-vitalscumulative-layout-shift

How can I prevent CLS with dynamic images of unknown height and aspect ratio?


I'm working on a news site where the images are changed frequently. I can control the width at which the images are displayed, but I have no way of knowing what their size will be from day to day and hence what the aspect ratio will be. One day I've got an image that's a perfect 3/2 ratio, the next day I've got one that is 153/300 in the same slot. This is leading me to some serious cumulative layout shift problems which I can't seem to solve.

Adding height and width attributes to the images won't help because the size can be so different from day to day. I've got them set to display at 100% width with height: auto. The aspect ratio is as I mentioned not something I can set ahead of time.

We are loading the images from a subdomain/media server onto the page itself. The entire site is running on a Django-based CMS.

I feel like I'm missing something here. Are my only options to either force the newsroom to use a fixed aspect size, do some sort of preprocessing of images (like an image CDN) to create a small set of aspect ratios, or maybe do something like placeholder images (would that even work in this use case when there's so much range of possible sizes?)? I hope I'm missing something simple, but....

Here's an example of the relevant markup and styling.

<article class="featured__story card">
<img src="our_media_server/static/photos/image_from_the_news.jpg?querystringforcache">
<div class="card__text">
  headlines, teaser text etc. all wrapped in a link to the article
</div>
</article>


.card {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    flex-direction: row-reverse;
    gap: var(--s0);
}

.featured__story img {
    width: 100%;
    height: auto
}

img {
    displaY: block;
    vertical-align: middle;
}

I'm actually using srcset for the images. The .cards use flexbox and the whole thing is in a grid layout, though I doubt that matters.


Solution

  • I would normally check the images server side, get its dimensions and add attributes accordingly. But if that's not possible, your hands are tied for sure. But a shift is a shift, even when your hands are tied. And measuring that (as it could distract users, forcing them to re-focus and potentially causing them to bounce) is the only purpose of CLS.

    What you could try do is going with a different UI. Give the image a fixed width+height using aspect ratio, and combine that with object-fit:

    .featured__story img {
        aspect-ratio: 3/1;
        object-fit: contain;
        width: 100%;
        height: auto;
    }
    

    You could pick an aspect ratio that will meet most image dimensions. Maybe add an onclick/fancybox feature to allow readers to still view the image in full.

    Additionally, you could add a blurred background using the same image to fill empty sides (or top and bottom).

    For example:

    <article class="featured__story card">
    <img src="our_media_server/static/photos/image_from_the_news.jpg?querystringforcache" class="blurred-bg">
    <img src="our_media_server/static/photos/image_from_the_news.jpg?querystringforcache" class="main">
    <div class="card__text">
      headlines, teaser text etc. all wrapped in a link to the article
    </div>
    </article>
    

    And the CSS:

    .featured__story {
      position: relative;
      overflow: hidden;
    }
    .featured__story img.blurred-bg {
      position: absolute;
      z-index: 1; /* not actually needed, but might be useful for other developers reading your code in the future */
      object-fit: cover;
      width: 100%;
      height: 100%;
      filter: blur(10px);
    }
    .featured__story img.main {
      position: relative;
      z-index: 2;
      aspect-ratio: 3/1;
      object-fit: contain;
      width: 100%;
      height: auto;
      display: block;
    }