go back

Placeholder for post cover image
Cover image for post

Generate placeholder images at edge with thumbhash

April 13, 2023

Continuing my journey to build a performant edge-cached blog! It's built on top of Cloudflare Workers, here was my last update on it:

It's been a fun sandbox to play with edge functions, and after coming across an interesting algorithm for minimizing images I was inspired to augment this project with it.

Pageload bottleneck 🌁

While using KV enables globally distributed data and shorter request latency, there's still one aspect of page performance that's a major bottleneck: loading images.

This tends to have the largest impact on performance regardless of server location, and in my case even though the data associated with each post is stored at the edge the image behind each URL must still be downloaded by each client.

I'm not looking to solve distributed image hosting (yet) so in the meantime, I built a simple image processing solution using thumbhash to store a tiny, base64-encoded version of the image to display while the full image is downloaded.

Enter: thumbhash πŸ‘

I came across this HN post from last month that caught my interest with some impressive performance stats. Here is the repository on GitHub:

GitHub logo evanw / thumbhash

A very compact representation of an image placeholder

While it's similar to BlurHash, the color performance is much better for the same filesize. Here's a a demonstration of this from the demo page (with ThumbHash in the middle and BlurHash on the right):

Side by side

Most importantly though, the blurred images are tiny (<1% original image size!). This makes it practical to consider storing them as encoded strings, which I intend to do. The project features an implementation in JS & I was confident it would be a great candidate for adding placeholder images to the site!

The implementation details below are specific to my project but the principles can be applied to anywhere there's server-side JS.

Generating thumbhash as part of edge caching process 🎞️

Building on the KV caching approach from earlier, a base64 representation of the cover image can be generated and included with post data. As stated before I don't want to worry about hosting images so storing the encoded image as a string avoids this.

Image manipulation is easy to do with client-side JS and canvas, but as this is on the server some additional packages make it possible. I used jpeg-js to decode the image:

import { decode } from 'jpeg-js'
import pica from 'pica'

const res = await fetch(imageUrl)
const arrayBuf = await res.arrayBuffer()
const decoded = decode(arrayBuf, { useTArray: true })
const { width, height, data } = decoded
Enter fullscreen mode Exit fullscreen mode

Then pica to resize it and a simple function I wrote to crop it:

const imageWidth = Math.floor((width / height) * 100)

const resized = await pica().resizeBuffer({
  src: data,
  width,
  height,
  toWidth: imageWidth,
  toHeight: 100,
})

const cropped = cropMid(resized, imageWidth, 100)
Enter fullscreen mode Exit fullscreen mode

Then all that's needed is to generate the thumbhash from this Uint8Array:

import { thumbHashToDataURL, rgbaToThumbHash } from 'thumbhash'

const thumbhash = rgbaToThumbHash(100, 100, cropped)

return thumbHashToDataURL(thumbhash)
Enter fullscreen mode Exit fullscreen mode

Nice! So now that this tiny blurred version of the image is on-hand the last step for a good user experience is to display it initially then replace it with the actual image once it's loaded in the background.

Loading the full-sized image 😴

Normally I would write some client-side JS to do this but as this project is solely server-rendered I opted to use a simple tried-and-true library for this: lazysizes.

This is as simple as adding a data-src attribute and lazyload class, with the original src set to the base64 thumbhash generated earlier:

<img 
  class="lazyload blur-up" 
  src={post.thumbhash} 
  data-src={post.cover_image}
/>
Enter fullscreen mode Exit fullscreen mode

Then after including the script tag in the renderer it Just Worksℒ️! I followed the steps to add the blur/unblur effect as well, as you can see from the blur-up class.


This is now active on blog.bryce.io, go check it out! And if your internet is too fast to notice it, try throttling to 'Slow 3G' via dev tools. Thanks for reading & stay tuned for the next thing I do with this pet project πŸ˜‡

go back