Image component: Avoid layout shift on image load

Description

We want developers who use frontity to prevent images from pushing the content under them when they load, whenever possible. We should provide clear options for the different uses cases.

The possible cases for an image would be:

  • Known dimensions (width & height)
  • Only height is known
  • Only the aspect ratio of the image is known
  • Only width is known / No dimensions are known

User stories

As a theme developer
I want to avoid jumps when my images load
so that I can give a better UX to my readers.

  • As a theme developer
    I want to add fixed height images to my theme
    so that I can avoid jumps when the images load.

  • As a theme developer
    I want to specify a default dimension for images
    so that I can reserve some space for those images that doesn’t have a known size.

  • As a theme developer
    I want to specify the image’s aspect ratio
    so that I can ensure, for any given width, that the height of an image won’t change after loading it.


I have also come up with the following user stories (although I don’t know if they fit well here).

As a theme developer
I want to preserve the style images have
so that I can assume an image will look the same as specified in the WP editor.

As a theme developer
I want to make images responsive
so that I can adapt their dimensions to any viewport.

EDIT: the user story about placeholders was moved to a new feature.

Possible solution

  • Known dimensions (width & height):
    In this case it’s actually straightforward, since it would be enough to assign these dimensions to the image. Dimensions could come from three places:

    • From width & height properties.
      Nothing more should be done than keeping those props in the Image component as they are in img.

    • From CSS (with width & height specified).
      Style of images could be preserved and thus maintain their size. It could be a problem if you want the image to be responsive.

    • From state → Moved to a new feature
      Entities saved in state.source.attachment[id] could have width & height stored in the media_details attribute. The Image component (in case the logic was inside it), should know the id of the image and access the state. If not, the code that renders the Image component should take care of that.

  • Only height is known:
    It would work the same as before, only that the width property cannot be specified (it could be left in auto). Even so, we would be preventing the image from pushing the content under it.

  • Only the aspect ratio of the image is known:
    Here you would have to create a container for the image that maintains that aspect ratio and at the same time adapts to the available width. It could be done in the Image component itself (right now it is being done by the image processor). In this case you could pass a prop called aspectRatio or something like that.

  • Only width is known / No dimensions are known
    Default dimensions should be assigned for this case, but the problem could not be completely avoided if the final height of the image is larger. It could also happen that the image has a height less than the one specified by default, so there would be an empty space below the image. You could either live with that problem, or maybe force the image to fit the default size, for example.

@David could you please add the different uses cases and user stories to the OP?

Done! I’ve just edited the original post.

@development-team, can you take a look at the post and give feedback? Thanks :blush:

Oh, there’s another case when the image has srcset and sizes attributes. I’ll document it later.

I love it.

@SantosGuillamot we should adopt this formatting for the rest of the features.

What do you mean by default dimension? Could it be more specific?

I’ve added a new feature for this so I removed it from here.

The rest looks fine to me :slight_smile:

@David does this need design?

By a default dimension I mean a default width and height that would be used until the final image is rendered, for those cases in which it isn’t known what size the image will be.

It should not necessarily be a pixel value, though.

Well, there is still the case of srcset and sizes attributes and I haven’t thought about that yet so, yeah, I think this need design.


PS: by design you mean to come up with a solution, right? Is this a task for a single person? How is the design phase? @SantosGuillamot do you have answers for these questions? :smile:

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>