Get an image with HTML fallback with caching?

250 Views Asked by At

I have a user image control, an identicon - if the user has an image set it should display it, if not it displays their initials with a coloured background (and some other HTML).

There are large numbers of these images, with lots shown at once, and also the same user might load multiple times on a page. Images can contain alpha transparencies and display against multiple different background colours. Images can be high-DPI and the fallback (and most of the other images they display next to) use SVG.

Images can change, but not often. Typically they can be cached for a few days or weeks.

I think there are two approaches to this:

  • JavaScript
    In this model the server returns JSON that I cache in IndexedDB, and the JSON contains a data-URI for the image if set. Additional JS is required to check whether a request is already being made for the same user and avoid repeating it.

  • HTML <object> fallback
    For this the server returns the actual image bytes, or a 404 if not found. Then if the image isn't found the content of the <object> tag renders:

#identicon,
#identicon > div { display: inline-block; width: 50px; height: 50px; line-height: 50px; text-align: center; background-color: red; overflow: hidden; }
<object id="identicon" data="api/users/joebloggs/photo" type="image/png">
    <div>JB</div>
    <!-- some more HTML and SVG content, snipped here -->
</object>

<object id="identicon" data="http://placehold.it/50" type="image/png">
    <div>PH</div>
    <!-- some more HTML and SVG content, snipped here -->
</object>

There are significant disadvantages for the JS option, as even if that JS is heavily optimised IndexedDB means callbacks (or synchronous options, like LocalStorage, tend to be slow). This involves quite a lot of JS for what should be a fairly simple task. It has one big advantage - it only asks for each user's photo once.

I like the simplicity of the <object> fallback, it has some minor drawbacks (lots of weird rules apply to how the HTML inside an object gets rendered), but one major problem: if the image isn't found it round trips the server again every time. A lot of users don't have images. Every page load gets a long stack of 404s for user images it should already know don't have one.

  1. JS is the best way, stick with it - any way to make it less JS/overhead?
  2. Leave the <object> with the 404s.
  3. Add an event to the <object> that fires some JS when it fails to load the image, but I'm not sure how to stop it trying again without reverting to (1)
  4. Change the server response to return a user image even when it fails.
    This loses the elegance of the fallback and means lots of server-generated images.
    This is what SO/gravatar does, but means some issues for us with high DPI screens.
  5. Change the server response to return a transparent image when it fails.
    This means the same image being cached lots of times and means layering content even for users that do have a photo. It also causes issues for images with alpha transparencies.
  6. Adapt the service worker to handle requests for user images as a special case, caching 404 responses and skipping the network round trip if they are attempted again.
  7. Abuse some other HTTP status response that <object> still sees as a failure to find the image and falls back to the content, but that is cached by the browser.
  8. Some other way?

All of these involve compromises, but I'm also fairly sure that I'm re-inventing the wheel here - chances are someone (in all the identicon implementations that are already out there) has already solved this. Can anyone point me in the right direction or suggest the best way to solve the problem?

1

There are 1 best solutions below

0
On BEST ANSWER

You can use a cache-control header with a 404 and the browser will honour it, and this check is much quicker than a call to localStorage or IndexedDB.

The solution is just to add the caching header to the 404 response. In .NET Core this is:

context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.Headers.Add("Cache-Control", $"public,max-age={duration}");

Then it won't go back to the server until the duration has expired, even though no image was found, <object> still falls back to its content and <img> still fires an error event.