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.
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:
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 ourimportmap.rb
file or by runningyarn 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 inapp/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.
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-blurhash
and theimage_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!