Image component: Avoid layout shift on image load

Just mention that srcset and sizes just gives information about the width of the image so it would be the last case (only width is known) if there isn’t any other way to obtain the dimensions.

I guess this feature doesn’t need design after all. :thinking:

1 Like

I think there might be another case where the width and height might be defined in the src query? Not sure if we encountered this before.

As for the solution for not known height, I think the only way to go is just defining a default height and default aspect ratio. I’d aim to something like 16:9, so the worst case scenario is a vertical image that is just too small.

Another solution to mitigate this could be to implement a zoom option within the component? It could be done with CSS or maybe just a link to the image? Even if that means to leave the blog?

Do you mean when the height is not known and a default aspect ratio is set?

So, for example, in this URL the lazy load is broken. That theme is using our image processor, but the images don’t have a height:

<img
  alt="captura de nuestro blog funcionado en localhost:3000 cargando un contenido de ejemplo"
  sizes="(max-width: 1024px) 100vw, 1024px"
  class="frontity-lazy-image wp-image-118"
  loading="lazy"
  style=""
  src="https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1024x700.png"
  srcset="https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1024x700.png 1024w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-300x205.png 300w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-768x525.png 768w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1568x1072.png 1568w"
>

The original block image doesn’t have a height anywhere:

<div class="wp-block-image">
  <figure class="aligncenter">
    <img
      src="https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1024x700.png"
      alt="captura de nuestro blog funcionado en localhost:3000 cargando un contenido de ejemplo" class="wp-image-118"
      srcset="https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1024x700.png 1024w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-300x205.png 300w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-768x525.png 768w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1568x1072.png 1568w"
      sizes="(max-width: 1024px) 100vw, 1024px" />
    <figcaption>Aspecto inicial de nuestro blog </figcaption>
  </figure>
</div>

What should we do in these cases?

Maybe the safest way would be to go back to the Intersection Observer implementation…

Ok, my bad… I have updated the Image component on that repo and it looks like that’s exactly what we are doing right now :sweat: :sweat::sweat_smile:

Sorry…

This the code now:

<img alt="captura de nuestro blog funcionado en localhost:3000 cargando un contenido de ejemplo"
  sizes="(max-width: 1024px) 100vw, 1024px"
  class="frontity-lazy-image wp-image-118"
  loading="auto"
  style=""
  src="https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1024x700.png"
  srcset="https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1024x700.png 1024w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-300x205.png 300w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-768x525.png 768w, https://horus.online/wp-content/uploads/2019/10/Captura-de-pantalla-2019-10-19-a-las-13.33.48-1568x1072.png 1568w"
>

And lazy loading works, so I guess it’s using the IO.

Actually here you have a case where you can get the aspect ratio from the image url. I know it’s kinda of a long shot, but it might make sense to cover cases like 1024x700 with a regexp or ?width1024&height=700 or ?w=1024&h=700 just parsing the query parameters.

But I’m a bit lost on what has IO to do with not having height. Did you decide to eagerly load images that don’t have height to avoid the jump later?

It looks like we fallback to IO when the image doesn’t have height because the native lazy load doesn’t work. I forgot about that. But we do lazy load it.

I think it makes sense to support aspect ratio extraction from the image URL. After all, we’re going to be dealing with images generated by WP or WP.com (Jetpack) most of the time.

Apart from that, we cannot use a container for the image because we break Gutenberg CSS. So we need to use an image placeholder instead. I hope that’s not a problem when using an aspect ratio, is it?

Is it possible to use <figure> as the container? The problem is that I don’t think we can make the images responsive maintaining the aspect ratio without a wrapper. At least I couldn’t find a way to do so.

It would be amazing if we could. Otherwise, we can search for wp-block-image with a higher priority processor and use its own wrapper. Then, try to get the images that are not included in a block. But that would complicate things…

For reference, these are the current Gutenberg variations of the image block:

  • With no-alignment (nothing selected), the HTML of an image is:
<figure class="wp-block-image">
  <img ...>
</figure>
  • With align left , the HTML of an image is:
<div class="wp-block-image">
  <figure class="alignleft">
    <img ...>
  </figure>
</div>
  • With align right , the HTML of an image is:
<div class="wp-block-image">
  <figure class="alignright">
    <img ...>
  </figure>
</div>
  • With align center , the HTML of an image is:
<div class="wp-block-image">
  <figure class="aligncenter">
    <img ...>
  </figure>
</div>
  • With wide width , the HTML of an image is:
<figure class="wp-block-image alignwide">
  <img ...>
</figure>
  • And finally with full width , the HTML of an image is:
<figure class="wp-block-image alignfull">
  <img ...>
</figure>

The library lazysizes has some recommendations to solve this:

It also has a plugin to use an aspectratio attribute. We can take a look at the code to understand what it does.

It looks like the Gutenberg team is going to solve the markup mess of the Image block: https://github.com/WordPress/gutenberg/issues/20650

I guess this will simplify the implementation for the fact that the aspect ratio can’t be applied directly to the img tag.

  • No Gutenberg -> add wrapper
  • Gutenberg -> use wp-block-image wrapper

Is that right guys?

It seems like there has not been much movement in this issue since November: https://github.com/WordPress/gutenberg/issues/20650 so maybe we should not wait anymore and try to fix this, as @orballo has proposed in this other thread

@cielosky42, could you help us here by making a list of the different variations of the current Gutenberg Image block, similar to the one I did a year ago here: Image component: avoid jumps on image load? Thanks!! :slightly_smiling_face:

Hi @luisherranz,

I will work on that. Thanks for looking into this.

Best,
Sky

Awesome, thanks :grinning_face_with_smiling_eyes:

I just run into this today. Maybe it’s a thing to take into account:

2 Likes

@luisherranz After going over this again, it does not appear that there have been any changes to the standard image markup since you documented it above. I think I was confused by the inconsistent image markup that depends on alignment.

I’ve further narrowed down issues with align left/right/center and non-aligned images.

For align left/right:

The images don’t display when floated left or right. The images themselves are absolutely positioned, and none of its parents have an explicit width set. If I set an explicit width on the wrapping figure element, the image is visible and is able to scale as needed when the constrained by the screen width.

The span in between the figure and img elements has both “width” and “height” properties set. However, this syntax is not valid on spans, only images. Was this meant to be added as CSS properties instead?

If so, I am seeing issues when trying to set the CSS of the span with the width and height. If I try going that route, the image does not display in the right aspect ratio as soon as there isn’t enough room for the full image size. It becomes distorted, or doesn’t scale, or has a bunch of extra padding, depending on other CSS added.

I’m not sure about all the other scenarios listed at the top of this thread, but as far as just adding an image block with left or right alignment, the only thing that works for me without running into the issues I mentioned is if I set an explicit width on the figure with a max width of 100%.

For aligncenter and images with no alignment:

Aligncenter and images with no alignment are filling the entire available space, greater than the width should be for the given size. Setting an explicit width on the figure also resolves the problem here.

Looking at the HTML2react image processor, I don’t see a way to modify the wrapping figure element, nor a way to target the image markup differently based on alignment.

Other than the previously mentioned CSS that overrides the “padding hack”, I was also able to “fix” this by targeting the size classes added by Gutenberg, which get added to the figure element. However, this requires adding specific styles for each avaialable image size. It would be nice if these extra steps weren’t necessary.

Here’s example CSS to fix things for the “medium” image size:

figure.wp-block-image.size-medium,
div.wp-block-image .size-medium {
  width: 300px;
  max-width: 100%;
}

Lastly, the images in the Gallery block suffer from these same issues. I was not able to apply the same fix as above with the gallery, because there aren’t image size classnames added to the figures in this case. The only solution I found here was to override the “padding hack”.

2 Likes

Thanks a lot for the detailed analysis @cielosky42 :slightly_smiling_face:

Thanks to you as well @orballo. Very interesting, although it seems like we would have to wait a while until all major browsers have support for this, right?


In my opinion, we have two different use cases here:

  1. The Gutenberg images inside of content.
  2. The other images added by the theme in React, like the Feature Image in a post or an archive.

For case 1, I see no reason right now to do anything on top of what WordPress+Gutenberg is already doing.

I’ve been taking a look and it seems like Felix Arntz added a fix to WordPress 5.5 to always include the image dimensions and avoid a layout shift:

I am not sure this is a 100% solved problem yet and there are still some related issues open in Gutenberg:

But my point here is: Why should we spend time solving an issue on top of WordPress if it can be solved in WordPress and there are people already working on solving it in WordPress? 🤷

@santosguillamot @cielosky42: what happens if we remove the image processor and simply use the Gutenberg CSS? Are the problems solved?


For case 2, I think we could provide an Image component that solves this for the rest of the images. Maybe we can design it to make it impossible to cause a layout shift.

I am not sure about the requirements for that, but I guess it would require at least one prop that it can use to figure out the space that needs to be reserved:

  • A fixed height.
    Used in conjunction with width: 100% to calculate the responsive size.

  • An aspect ratio.
    With the same value as the upcoming CSS prop (https://web.dev/aspect-ratio/) but used internally to calculate the padding hack value.

  • A media ID.
    So the Image component can access state.source.media[id] and get the width and height from there.

This is just an example. I am not a CSS expert by any means and less with this image size sorcery, so maybe I am wrong in my assumptions.

EDIT: This seems like a great resource to learn how to calculate the padding hack using an aspect ratio input: Aspect Ratio Boxes | CSS-Tricks

Can we use this one code for ACF fields in image . because i have passed id value but i didn’t get attachment data from state.source.attachment[id] API.

Implement lazy loading for images, which defers the loading of offscreen images until the user scrolls near them. This can significantly improve page load performance and minimize layout shifts by loading images only when they are needed.