Better image placeholders with blurhash in Active Storage

By Exequiel Rozas

When it comes to user experience, perceived performance is as important as the actual performance itself.

That's why techniques like loading indicators, placeholders, or loading skeletons help mitigate the wait until resources actually load into our screen.

In this article, we will explore the use of the so called BlurHash pattern in Rails using Active Storage with the active_storage-blurhash gem, even though the technique can be used with any other file upload library we will focus on Active Storage and this gem.

Special thanks to Julian Rubisch for creating this gem.

Let's start by finding out what Blurhash is and how it works:

What is Blurhash

It's a compact representation of an image that's meant to be used as a placeholder for the actual image while it loads.

Because of its small size, usually between 20 and 30 characters, and as the name implies, it's supposed to look like a blurred version of the actual image it represents.

When the actual image is loaded, the blurred version of the image fades out while the image fades in, creating a seamless effect which gives our users an improved experience over showing a gray placeholder or skeleton.

Blurhash image transformation

As you can see, the original image is split into several chunks, which simplifies the image, thus reducing the available information to render. The blurhash representation of the image is minimal compared to the image, but it still captures the image's essence while the colors blend pleasantly.

What we will be building

We will build a simple application called drivel, which is basically a list of posts with prominent images:

Example application showcasing a list of posts with prominent images

The only view we will define is the root view, that displays a list of posts with an image, a title and some fake likes and views count.

The average image size here is around 4 MB to dramatize the effect of the blurhash loading effect.

After implementing everything we will see in this tutorial, the final result should be the following:

Using the active_storage-blurhash gem, we get no layout shift: the image size is defined before the images finish loading.

We also get a blurred preview of the image almost instantly and the images itself, which are big, are transitioned smoothly whenever they finish loading.

So, after seeing this, let's start building the app:

Building the app

Because the gem allows us to use both import maps and JavaScript bundling, we will start our application using the former:

$ rails new drivel --css=tailwind --javascript=importmap

Then, we need to install ActiveStorage to handle our file uploads:

$ bin/rails generate active_storage:install

We won't be going into more complex topics like S3 uploads with Active Storage because that's not necessary to implement the blurhash feature.

So, after running the Active Storage installation command, we migrate the database to create the tables we need to store and process (analyze) the images:

$ bin/rails db:migrate

We add a simple Post model to have the images associated to it:

$ bin/rails generate model Post title:string

Then, we associate our model with Active Storage using the has_one_attached method:

# app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

Next, we add the active_storage-blurhash gem to our Gemfile. Note that we also have to add the image_processing gem to generate the blurhash for each image:

# Gemfile
gem "active_storage-blurhash"
gem "image_processing"

Then we install active_storage-blurhash by running the following command:

$ bin/rails g active_storage:blurhash:install

If it runs successfully, it will:

  • Install the blurhash Javascript dependency, either by pinning it to our importmap.rb file or by running yarn add blurhash in case we're not using import maps.
  • Copy the JavaScript code in charge of conditionally displaying the blurhash or the image once it fully loads into app/javascript/blurhash/index.js.
  • Make the JS code available to our application by either pinning it to our importmap.rb file and importing it in app/javascript/application.js or just importing it.

Now, we could theoretically use the library-defined blurhash_image_tag helper and everything should work, however…we haven't generated any blurhash yet.

Good thing is, the gem comes with a command that runs a rake tasks that allows us to backfill our existing images and populate the blurhash metadata attribute:

$ bin/rails active_storage_blurhash:backfill

What this command does is run a custom Active Storage analyzer, a special class able to extract metadata from blobs, that generates the blurhash attribute for the image and stores it in the corresponding ActiveStorage::Blob instance.

If our application has many images, and we need to process them in batches, we can pass the BATCH_SIZE environment variable to the command:

$ BATCH_SIZE=50 bin/rails active_storage_blurhash:backfill

Note that the task is run asynchronously within an AnalyzeJob which is added to our job queue.

If you need to double-check or run the code by yourself, this is what gets executed when we run the command:

batch_size = ENV["BATCH_SIZE"]&.to_i || 1000

ActiveStorage::Attachment
  .joins(:blob)
  .where("content_type LIKE ?", "image/%")
  .find_each(batch_size: batch_size) do |attachment|
  attachment.analyze_later
end

This finds every image blob stored in our database and runs the custom analyzer, which generates a thumbnail to speed up the image analysis, generates a blurhash using the blurhash gem and stores it in the metadata.

Then, we can call the blurhash tag helper in our views to handle the rendering for us:

<%= blurhash_image_tag post.image %>

Behind the scenes, this helper returns the following structure, which is picked up by the custom blurhash/index.js script to show and hide the blurhash/image accordingly:

<div data-blurhash="the-blurhash-content">
  <img src="image-source.jpg" lazy="true" width="image-width" height="image-height" />
  <canvas width="thumbnail-width" height="thumbnail-height" >
</div>

As you can see, the active_storage-blurhash gem comes with everything you need to add the blurhash feature to your Rails app that uses Active Storage.

Behind scenes, the image_processinggem uses ImageMagick/GraphicsMagick or libvips to perform the actual processing. Make sure to have them installed on your system or follow the instructions on the image processing repo.

Generating the blurhash when adding new images

We generated the blurhash for existing images using the backfill command. However, we need a way to generate the blurhash for new images.

To achieve that, we could have a scheduled task running every X number of minutes to run the backfill task, but that's a bit too cumbersome for our purposes.

So we can schedule the analyze_later job after the image is created, in our case we can do that in an after_commit callback that runs on creation and update.

class Post < ApplicationRecord
  has_one_attached :image
  validates :name, presence: true
  after_commit :process_blurhash, on: [:create, :update], if: :image_attached?

private
  def process_blurhash
    return unless image.attached?
    image.analyze_later
  end

  def image_attached?
   image.attached?
  end
end

TL;DR

Adding a blurhash feature to a Rails app is a good way to improve the user experience, especially for those users with slow internet connections.

We could implement the feature ourselves, using the blurhash gem and some custom JavaScript to handle the appearance of the image and corresponding disappearance of the rendered blurhash.

Luckily, we can use the active_storage-blurhash gem which solves the issue for us so we can focus on what makes our app special.

To implement the feature using the gem we need to:

  • Install active_storage-blurhashand the image_processing gem.
  • Run the bin/rails active_storage_blurhash:install command which takes care of adding the blurhash JavaScript dependency and the gem-specific implementation of the front-end code.
  • Make sure to backfill our existing images so the blurhash is present in each attachment's Blob metadata.
  • Add a way to generate the blurhash representation for new images we might add.
  • Use the <%= blurhash_image_tag %> helper instead of the traditional <%= image_tag %> wherever we wish to have a blurhash placeholder.

And that's about it, now your UX is a little bit better and you didn't have to spend too much time to achieve the feat.

Hope you enjoyed the article, have a good one and happy coding!

Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.