BlurHash as a service with Cloudflare Workers
Exploring the UX and implementation of displaying blurred images while loading.
Last week I came across BlurHash on twitter. It’s an interesting tool for dealing with image loading issues. Basically it allows you to show a blurred version of an image while the real image is loading over the network. So you can show a kind of preview while it’s loading.
This is a pretty useful technique in terms of UX and perceived performance. It obviously looks a lot nicer, since you’ve got pops of colour and a bit of variety, but the more important part is that users can actively see that the website is loading.
When a user sees just a blank screen or a blank area they can’t get a sense that the page is loading. Maybe it’s broken, or stuck. If you introduce intermediate loading steps it feels to the user like there is an active loading process going on. This is the same concept behind skeleton screens, where a skeleton of the user interface is displayed while the page is loading.
The way BlurHash helps you out is through tooling, not through some specific component implementation. BlurHash gives you two main functions: an encode
function and a decode
function. The encode
function turns the image into a short string of characters (similar to a hash), while the decode
function turns a string into a blurred image. Here’s the diagram from their website:
What’s really cool about this is that you can generate the BlurHash string server side when you’re processing images, and then save it along with your model. Then on the client side you can render the blurred image while you’re waiting for the full image to load. Since the blurred image is just a string, you don’t need any sort of binary storage or transfer - just throw it in your JSON or your HTML and get on with it.
The situation this doesn’t work for is when you don’t have full control over server-side processing of your images. You might be consuming images from somebody else’s API, or you outsource your image uploads. I’ve been messing around with Cloudflare Workers a lot recently and it struck me that it would be pretty cool to have a worker do this processing for you.
Thinking with Workers
If you haven’t come across Cloudflare Workers it’s a super interesting web technology. Cloudflare has a large number of servers that are part of their “edge network” - this basically just means servers which are really close to users (low latency) on very fast network connections (high bandwidth). With Cloudflare Workers you can write a small chunk of code that runs on their global edge network and responds to HTTP requests. If you’re into web performance - this is a really cool technology to be across.
Here’s a quick summary of what I’m thinking in terms of front-end developer experience using this Cloudflare Workers BlurHash wrapper:
Developer loads an image by URL (e.g.
example.com/image.png
)While the image is loading, they ask the worker for the BlurHash for that URL
They use BlurHash
decode
to generate their blurred imageWhen the original image finishes loading they replace the blurred image with it
For this to be useful the roundtrip to the worker needs to be faster than the image will load on the client side. Fortunately Cloudflare Workers includes a Key-Value store (KV) which I can use to cache the BlurHash if it’s ever been generated before. The KV store is globally persisted, so once I’ve done it in one place it will be persisted everywhere. This means I should be able to get millisecond response times from the Cloudflare Worker.
From the worker side that means our implementation will look something like this:
Receive a request for an image’s BlurHash (e.g.
example.com/image.png)
Check if that key exists in the KV store
If the key exists, then return the BlurHash value stored there
If the key doesn’t exist, fetch the image, generate the BlurHash, store it in the KV store and then return the BlurHash value
A proof of concept
In this post I’m going to walk through building a proof of concept for this idea. The goal for the proof of concept is to take a URL, retrieve it and generate the BlurHash. Once I’ve done that it will be relatively easy to add the KV as a cache. To start with I’ll need to generate a new Cloudflare Workers project using their CLI tool wrangler
. You can read more about using Wrangler in the Workers docs.
I’ve used the typescript template for a Worker because I’m pretty comfortable with it but there’s other options. I’ve ended up with a directory structure like this:
The file most interesting here is src/handler.ts
this is where you implement the code for handling requests. Let’s have a quick look at it:
It’s an async
function that takes a Request
object and returns a promise for a Response
object. If you’re familiar with Node then this is probably looking a bit strange, but if you’re more of a front-end person this might look a lot like how the fetch
API works. It’s probably time to do a quick little aside about Cloudflare Workers.
Cloudflare Workers don’t run Node
This may be shocking to you, it may feel strange to be outside of the Node comfort zone. Fortunately, things aren’t too weird. There’s good reasons why Cloudflare Workers aren’t Node compatible, which I won’t get in to, but you can read about in the docs. Cloudflare Workers are still built off V8, so they support all the standard library features that Chrome supports (minus all the “browser” stuff).
You can still use npm
and you can still use all your normal JavaScript ecosystem tools (like TypeScript). Just sometimes packages that are made for Node won’t work. Fortunately most packages are built to be compatible with Browsers and so will just work in Cloudflare Workers. This can be a bit of a bummer, but the end result is that Workers are really fast. They don’t have to wait for Node to boot. In fact they have an incredible zero cold start time you can read about that here.
Instead of being built like Node apps, Cloudflare Workers are built using the Service Workers spec. This ends up being quite a neat choice, since the API is very tidy. Ultimately it ends up being pretty simple to understand and takes advantage of the latest JS features.
Requesting an image
The first thing I want to do here is to grab an image. I’ll need the image to be passed in as a search query, like ?image=example.com/image.png
. Then I’ll go and fetch that image.
You can see here that I’m just using the standard library tools that you would use in JavaScript on the frontend. I’m taking the url
string out of the Request
object and turning it into a URL
object. From there I can grab the URL of the image that was passed in as a search parameter like ?image=example.com/image.png
.
Once I’ve got the image URL I can use fetch to grab it:
Now I’ve got an actual image I’ll need to pass it on to BlurHash, so let’s look at the docs for BlurHash and the encode
function. It looks like this:
Wait… it needs… raw pixels? And the width and height of the image?
Loading in the raw pixels of an image means decoding the image from PNG, JPEG, or whatever else it is, which sounds scary! The part I’m most worried about is Cloudflare Workers compatibility. But after some googling I found a little tool called pngjs
, which seems to have some sort of sordid history in the JS community (there’s a lot of forks!).
It can take the file stream of a PNG and decode it, then I can extract the pixel data, width and height from the image. I won’t be able to do other image formats, but PNG is good enough for a proof of concept. The problem is that the documentation says I should use it like this:
Unfortunately inside Cloudflare Workers I don’t have either a) a file system, or b) the Node Stream API. My stream comes from fetch
which uses the web’s Streams API. With a bit of reading both the Node documentation, and the MDN documentation I was able to put together this little snippet:
This creates a new PNG object, and then writes the chunks of the image into it as they come in from the stream. Once the stream is done I mark it as ended. Effectively I’m implementing a bridge between two different stream APIs. The last thing to do here is to wrap all of that in a function.
There’s a bit of tomfoolery there with the image.data
but I’m pretty sure it will work. It’s not ideal, but it’s a difficult situation since I’m dealing with weird boundaries between Node, Cloudflare Workers and TypeScript.
Now I’ve got the data I need it’s time to make a hash out of it!
And… that’s it I guess! Let’s have a look to see if it works.
Wow! That absurd string (U6R{;=xZ^b%eKnWB-ntSm$ozafV@^ZoeD+RO
) looks like the result I expected. But I want to be sure, so I’m going to check that other implementations of BlurHash generate the same result. Fortunately they have a CLI version as part of their C library.
That’s the same string! It works! We did it! Proof of concept… proved.
Learnings
This was a fun bit of exploring. This proof of concept has shown that it’s possible to generate a BlurHash on the fly using Cloudflare Workers. With some additional work I could cache the BlurHash using KV store to make sure responses are as fast as possible. It would also be helpful to add a batch mode to the API so that a number of image’s BlurHashes can be retrieved at once to reduce web request overhead.
If I wanted to use this in production I’d also need to handle arbitrary images. It looks like there are some Web Assembly libraries that I could use for this, which I’d need to check out. I’d also want to reduce the time taken to calculate the BlurHash by resizing the image down to 32x32 first (as recommended in the docs). This could be done with Cloudflare’s image resizing, or with an image processing library.
Finally, Cloudflare Workers does have some pretty strict resource limits. If the image size was small (like 32x32) an image would take up about 4096 bytes ( 32 * 32 * 4 bytes for each of rgba), which wouldn’t be an issue. But if we were operating on larger images using 16-bit colour they might inflate out in memory when decoded to raw pixels.
Post-amble
Recently I’ve been spending time mucking about with new technologies, and I’m thinking that I should write these things up. If you found this interesting you should subscribe and give me some feedback on this post.
Cheers and thanks!
Tried this but pixels is always returning null for me.