Image component: Avoid layout shift on image load

We have a first idea but I guess we’ll have to improve it. This is what I have in mind right now, @luisherranz may have more things in mind. I’ll try to document this in other place once it’s clearer. Any feedback is welcomed:

  1. Before the dev design, there is a process where we have to discover what the users need and define the user stories. This first part is a previous work to develop the feature.

    • For example, if users ask for the comments package, we’ll investigate and talk with them until we know that they may need to be able to order them by newer/older, select how many comments to show, etc.
  2. Once we know the goal of the feature and what functionalities are needed, we have to translate that into a technical solution. If there is a lot of uncertainty, the technical solution is not clear, and we think we are going to struggle to divide it in issues/tasks and estimate it, we should start a design phase to solve most of the questions.

    • For example Support for Middleware in Frontity Connect may need design but the comments package not, because although we haven’t come up to a solution yet, it shouldn’t be difficult to estimate and divide it once we know what users need.
  3. If we decide a feature needs design, one person will be responsible of it, but there’ll be a discussion here to discuss about it. The responsible person would analyze it, prepare some questions that need to be answered and suggest some possible solutions and the different reasons. From there, everyone can give feedback until we reach a solution where we feel comfortable. Once the design is done, we will open a time-box for RFC.

    • For example, it could be something similar to what you did with Emotion and Styled Components.
1 Like

Ok, it’s much clear for me now. Thanks @SantosGuillamot!

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