Image placeholders

When I was experimenting with the swatch chart feature in the paint library I realised that loading the swatch images could take a while, no matter if they were resized to smaller size (and I did not want to compromise on quality). Even with a relatively fast internet connection it could take seconds to load all the images - and until that, the color chart looks broken.

There are multiple techniques to make this issue less prominent, I only list here what I’ve found relevant for my use case.

Just define the image dimensions

Defining the proper image dimension helps, but it looks weird. When I visit the chart page I want to see a chart not some empty spaces.

Generic image placeholders

The placeholder image can be a generic image, used for all images so downloaded only once, which implies that something will be shown here soon. Seems dumb, but at least it shows something will be loaded there.

Dominant color

Slightly better, if we could extract a dominant color from the swatch and show that as a background. There are ruby libraries for that, but it’s kind of hard to pick a single dominant color even for a watercolor swatch.

Progressive jpeg

I gave a try to progressive jpegs. It supported by most of the browsers, and you can use imagemagick to convert images. However, I did not like the experience and never knew when the image was fully loaded, especially because watercolor swatches sometimes looks ‘broken’ by default. :)

Responsive images / srcset attribute

Also experimented with responsive images and that still does not solved my issue.

Low-quality image placeholder techniques

There are many LQIP techniques, basically it means that on the initial page load, some lower quality - but recognisable image is shown, which is replaced later by the ‘real’ image.

Super-small low quality image

We can generate a super-low-quality version of the image, and show it as a placeholder until the real image is loaded. (The small image could be even part of the initial html, base64 encoded) The issues I’ve found with that approach, that upscaling / blurring can be an additional effort, and the file size still not small enough, and I had the same issue with progressive jpegs, it looked bad for my use case.

Blurhash

But then I read about blurhash. It’s a great idea for my case: you can generate a relatively small hash representation of your image, serve this hash to the client, decode it for a blurry image placeholder.

I don’t see this approach would work for me other type of images, like photos, since the blurred image is not recognizable. However for watercolor swatches, it’s just perfect.

Unfortunately this approach requires javascript.

Blurhash offers libraries for multiple languages for encoding and decoding. Encoding normally happens on the backend side, ideally as some pre-prepocessing of the image. Decoding usually happens on the client side, the browser just receives the tiny blurhash string, and creates a blurry image on the fly.

Server side rendered blurhash images with Rails 7 + stimulus

I was trying multiple approaches for both decoding and encoding and find this one as the easiest method as I’m mostly using server side rendered pages.

There are multiple ruby gems, and I chose the ruby-blurhash. It’s not listed on the blurhash page. Written in c, fast, and most importantly, it supports both encoding and decoding. Also it has no dependencies.

Encoding the images

That needs to be done once. When I’m adding swatch images, I do some pre-processing anyway, like adding EXIF IPTC information to files, resize them, etc. In this step I also calculate the blurhash, and save it to the database. It’s a super small string (max 30 characters). But what I’ve found, actually the decoded base64 representation of the placeholder image is also small enough (~150 bytes) so I thought - in my use case - why bother to always decode, when I can just save the decoded placeholder image, and serve it immediately? So I ended up simply saving the decoded blurhash image.

Here’s an example image: PB33-swatch.jpg the file size is 310.810 bytes

blurhash = BlurhashRuby.encode_image('PB33-swatch.jpg')
#  => "LE2,{oL%tKV[Qno|bbaLodbHaLoc"
paint.swatch_decoded_blurhash = BlurhashRuby.decode_blurhash(blurhash, as_img: true)
#  => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAAJklEQVR4XmNkKL/0n4GLlYGBnZVBjpeJgYWBB8hhYwEL/OFlYQAAUuQDyl0V3zQAAAAASUVORK5CYII="

The blurhash representation is 28 bytes, the base64 decoded blurred image 128 bytes.

Displaying the blurhash images

Displaying the image now super easy. When I render the page, I can just add the blurhash image directly from database:

image_tag(paint.swatch_decoded_blurhash)

The rendered HTML image stg like this, that will be delivered to the client when downloading the page.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAAJklEQVR4XmNkKL/0n4GLlYGBnZVBjpeJgYWBB8hhYwEL/OFlYQAAUuQDyl0V3zQAAAAASUVORK5CYII=">

Replacing the blurhash image with the real image

We have the blurhash images displayed, but we want to replace the with the real swatch image asap. So let’s pass the original image url along with the image tag:

image_tag(paint.swatch_decoded_blurhash, class: 'lazy-image', 'data-src': paint.swatch_url)

The last remaining step is a piece of javascript, to replace the the image src with data-src. I use a stimulus controller for it:

import { Controller } from "@hotwired/stimulus"`

export default class extends Controller {
  connect() {
    Array.from(document.querySelectorAll('img.lazy-image')).forEach(image => {
      var loaderImage = new Image();
      loaderImage.onload = () => {
          image.src = image.dataset.src;
      }
      setTimeout(() => {
          loaderImage.src = image.dataset.src;
      }, 1000)
    })
  }
}

Now when the browser loads the page, the placeholders are immediately visible, and will be replaced later. I’m pretty sure that piece of javascript can be improved :) but it did the job.