Graceful fallback for avatars (404 no more!)

Julian Lam 10/15/2022, 9:17:00 PM

In the midst of our work hacking on v3 (and I mean that in the best possible light), we ran across an interesting problem with an even more interesting solution.

Before I get into that, reflecting on this recent problem reminded me that I haven't talked about our development process in a little while.

I thought it might make an interesting exercise to step through my thought process and demonstrate a couple of neat developer habits that worked well. Sound good? Let's go!

Here's the problem πŸ‘‰ sometimes user avatars don't load

A screenshot demonstrating a few missing avatars

There could be myriad reasons why that is the case, from CORS miconfigurations, to network congestion, or just the most common reason of all, that the file on the other end just no longer exists.

For the longest time, we simply ignored the problem. It wasn't significant enough to pose a usability issue, and usually affected a very small subset of user avatars β€” as you can imagine, active users tend to want to have a working avatar.

However, when setting out to resolve this, we wanted to find a solution that allowed for falling back to the user icon (the coloured circle with the single letter). This complicated matters somewhat, due to how our avatars are generated.

The buildAvatar helper

We use the buildAvatar helper as a shorthand in our templates to generate a consistent avatar. It was introduced many years ago to enforce consistency across the Persona theme, as each invocation of the user icon had its own idiosyncrasies. In and of itself, the helpers still does allow for some customization. You can customize the size, shape (square or round), classes, attributes, and even specify an alternative component.

However, the helper did enforce one thing: it displayed an avatar, or if one was not available, it displayed the user icon.

The avatar was an <img /> element, and the user icon was a <span> element. As opposed to our competitors who dynamically generated an image on the server-side, we opted to keep things simple and use plain HTML and CSS.

Solution #1 β€” A fallback image

If an image failed to load, we could load a fallback image (at this point, how we went about doing that was not considered).

We moved on from this solution because we wanted the appropriate fallback to be the user icon, and not a hardcoded image substitute.

Solution #2 β€” server-side enforcement

The first solution to mind was to have the server check that the image was loadable. The server could even act as a caching layer, in case the remote image went down.

In the end we discounted this solution because of a few factors:

  1. It added additional load onto the server to check the image.
  2. Uncertainty over re-checking the image to ensure that it still loads
  3. Uncertainty over how long to cache the image (if we decided to do that)

Solution #3 β€” Pseudo-element shenanigans

Continuing my research on handling images that fail to load, I stumbled upon a "solution" that utilised CSS pseudo-elements to generate a fallback image or placeholder.

There were a couple red flags for this one:

  • Pseudo-elements don't actually work on image elements (that's a pretty big red flag)
  • The "solution" actually relied on undocumented behaviour whereby pseudo-elements actually DID work for image tags, but only if the image did not load (a perfect fit!)
  • The undocumented behaviour actually didn't work on Firefox, so that's a pretty solid nail in that coffin.

Solution #4 β€” Regenerate icon on image error

I then turned my attention to doing a client-to-server round trip to regenerate the icon. Since we called the buildAvatar helper to generate the image the first time, we could retrieve all necessary data and call it again (just without the broken picture url).

However, things got complicated quickly when I realized that we didn't have the original context for the image.

If all we had was the user picture, how could we get the appropriate user data to call the helper again?

But wait! The generated HTML contains a data-uid paramater, we're saved!

While we did have the user id, and that technically was enough data to do that round-trip to get the user data, we didn't easily know what arguments were originally passed into that buildAvatar helper (e.g. whether it was round, what size, etc.)

But not to worry! We could easily parse the attributes back out from the generate HTML, so we could accurately pass those back in...

Let's pause for a minute... πŸ•“

You see where I am going with this, yes? In my quest to resolve this (admittedly small) problem, I went through a number of solutions, each more complicated than the last.

It's very easy to fall into this type of trap, where you dig yourself deeper and deeper attempting to overengineer a solution. It's essentially the sunk cost fallacy, I've already spent X amount of time on this, what's another couple minutes/hours?

It was precisely at this point that I practiced what I consider the holy grail of developer tools β€” the rubber duck. I stepped through the problem with my colleague Baris, mostly out of amusement, and quickly ran through my attempted solutions.

At the same time, I stepped back. All of the above had been occurring over the past couple of hours, and there are times when you are so stepped in the context of the problem that you cannot see the forest for the trees.

So what happened? In the span of five minutes, I came up with the actual solution. A solution that surprised me in its relative simplicity.

Solution #5 β€” the double avatar

buildAvatar does exactly one thing and it did it well: it consumed user data, and it output either an image at a specified dimension, or a user icon with the appropriate CSS to match the desired dimensions.

Either way, an element with known dimensions was generated, so it did not matter whether an image url was available or not.

Knowing this, it was technically possible to generate both the pictureΒ in addition to the user icon, and simply hide the second using CSS. If a user's picture fails to load, we can simply remove it, and the user's icon would magically take its place, since it's no longer hidden because it's no longer the second avatar.

Here is the commit where buildAvatar is updated to show both avatars (with the second one hidden), and the corresponding commit where on error, the image itself is removed from the DOM.

I love when I can replace potentially large solutions with relative one-liners. Happy hacking!

tl;dr

  • Generate the user avatar and the fallback image (or our case, the user icon) side by side
  • Use this CSS to hide the second image
    .avatar +.avatar {
        display: none;
    }
  • Add an onError attribute to the potentially faulty image that contains this.remove()
  • Now any broken images will be removed from the DOM as soon as the browser determines that it is invalid, and your fallback will take its place
  • Note: Astute readers will realize that that specific CSS will always hide any avatars following the first avatar, so this would not work if you had a string of avatars in a row. In practice, we wrap all of our avatars in anchors, so this defect is mitigated, but it is something to think about.