Dennis Schubert

Observing Broken Image Intersections

2018-11-10 mozilla, webcompat

So, I thought I am going to share this little story about a broken website I looked at recently, just to make an example of how weird the web can be, and how Gray implementing specifications can sometimes be.

Imagine a site1 with a list of news articles and lots of thumbnails next to them, and imagine the served code looks something like:

CSS:

.thumbnail {
  overflow: hidden;
}

.thumbnail img {
  height: auto;
  margin-left: -42px;
  width: 213px;
}

HTML:

<div class="thumbnail">
  <img data-lazyload-src="foo.jpg" />
</div>

That does not look too fancy, does it? The data-lazyload-src attribute on the image suggests they are doing some kind of image lazyloading, but that is probably a good thing given the site is mobile optimized as well, and you do not want to load a lot of thumbnails at once. The site is actually pretty smart about it, and is using a library that implements an IntersectionObserver to be notified whenever an <img> scrolls into view, to then trigger loading the image. Pretty cool stuff.

Now, the fun part. We received a report that the site is working fine in Chrome, but for some reason, in Firefox, the thumbnails never load. Pretty bad.

After evenly distributing breakpoints in code I deemed relevant, it turned out the IntersectionObserver never triggers the image loading. As my knowledge about the IntersectionObserver was still stuck in 2014 (which is pretty much nothing, given the work on it started in 2015), I took the time to read the spec, because clearly, Firefox has a compat issue breaking that website. And well, I actually found a compat issue, but that one was completely irrelevant to the issue I was originally debugging.

So, back to the beginning. Looking again at their IntersectionObserver, I realized that Firefox is calling the callback for a lot of images, but in Firefox, IntersectionObserverEntry.isIntersecting is false, even for the images that should be true as they are scrolled into view. In Chrome, everything is fine, and some of the thumbnails are reported to be intersecting.

Before you scroll up to check the source code again, let me remind you that images should be rendered as display: inline; per default, as you surely remember2. Now, what do you expect the CSS to do in the default case where no image is loaded:

  1. Scale the image to 213px width with some magic height.
  2. No dimensions applied to the image, because it is display: inline;, d’oh!
  3. Render a “broken image” icon, but it is replaced with a picture of a cute kitten.

If you guessed 1, 2, or 3: Congratulations, you are wrong! As we all know, CSS is easy, and this is one of those cases where CSS is super easy. So, let me explain this simple CSS behavior by talking spec for a second here. <img> is, amongst some others3, a so-called replaced element. The spec accurately describes those as

An element whose content is outside the scope of the CSS formatting model, such as an image, embedded document, or applet. For example, the content of the HTML IMG element is often replaced by the image that its “src” attribute designates

which is basically the spec authors telling you “yeah, we also do not know how it looks like”. For images, there are some rules on how the browser should render things:

  • If the element does not represent an image, but the element already has intrinsic dimensions (e.g. from the dimension attributes or CSS rules), and either:

    • the user agent has reason to believe that the image will become available and be rendered in due course, or
    • the element has no alt attribute, or
    • the Document is in quirks mode

    The user agent is expected to treat the element as a replaced element whose content is the text that the element represents, if any, optionally alongside an icon indicating that the image is being obtained (if applicable).

  • If the element is an img element that represents some text and the user agent does not expect this to change

    The user agent is expected to treat the element as a non-replaced phrasing element whose content is the text, optionally with an icon indicating that an image is missing, so that the user can request the image be displayed or investigate why it is not rendering. In non-graphical contexts, such an icon should be omitted.

  • If the element is an img element that represents nothing and the user agent does not expect this to change

    The user agent is expected to treat the element as an empty inline element. (In the absence of further styles, this will cause the element to essentially not be rendered.)

There are some nasty spec language bits in there, but in order to not bother you more than I need, I will skip those, but you get the idea. If you scroll back up to the source, you will notice two things: the image tag in question does not have a src attribute, and to add more fun to the mix, it also does not have an alt attribute, but it does have intrinsic dimensions, as they are defined via CSS.

So, technically, the first case is true: the element is not an image, and it also does not have an alt attribute. But what does “treat the element as a replaced element whose content is the text that the element represents” even mean? How are we supposed to replace nothing with text? Because there is no text, the last case is also true, because there is nothing there, and because there is no src attribute to be loaded, the browser also does not expect this to change.

To my understanding, this means the browser can replace the element with either something or with nothing. Well, let’s see what different browsers do:

HTML:

<img />
<hr />
<img src="broken image!" />
<hr />
<img alt="" />
<hr />
<img alt="poetic alt text" />
<hr />

Comparison of the code example&#x27;s rendering in Firefox, Chrome, Edge, and Safari

As it turns out, browsers disagree in our relevant case. In Firefox, we render nothing as an inline element, and Chrome decides to render something empty as inline-block.

Even worse, I am having a hard time figuring out who is right and who is wrong here. There are two Chrome issues (one, two) about this specific scenario, and a Firefox patch landing just as I write this that brings Firefox closer to Chrome, at least in the no-src scenario. But still, there seems to be a general disagreement on what the right thing is.

To end this whole post: if you paid attention4, you have figured out the original issue by now.

Because Firefox renders nothing (that is actually not entirely true, but let us act like it is, because the reality would turn this post into a proper scientific paper), there is nothing that can ever intersect the viewport, so the IntersectinObserver returns, rightfully so, false. On Chrome, however, there is something that is 213px wide, so there is something that intersects the viewport, so there is something for the observer to report on.

And there is our issue. Quite simple, eh?

The sad thing out of all is there is a very, very simple solution to all of this.

.thumbnail img {
  display: inline-block;
}

And they would live happily ever after.

Footnotes

  1. This is totally not webcompat.com bug #18554, and I am totally not trying to write a miketaylr.com style blog post here.

  2. Yeah, me neither.

  3. audio, canvas, embed, iframe, input, object, and video, if you really want to know.

  4. Yeah, me neither.