Generate placeholder images at edge with thumbhash
Tags: webdev, javascript, showdev, serverless
Published: 2023-04-13 18:40:51

Continuing my journey to build a performant edge-cached blog! It's built on top of [Cloudflare Workers](https://workers.cloudflare.com/), here was my last update on it:

{% embed https://dev.to/bryce/using-workers-kv-to-build-an-edge-cached-blog-23fo %}

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](https://web.dev/learn/images/performance-issues/) 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](https://github.com/evanw/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](https://news.ycombinator.com/item?id=35265752) from last month that caught my interest with some impressive performance stats. Here is the repository on GitHub:

{% embed https://github.com/evanw/thumbhash no-readme %}

While it's similar to [BlurHash](https://github.com/woltapp/blurhash), the color performance is much better for the same filesize. Here's a a demonstration of this from the [demo page](https://evanw.github.io/thumbhash/) (with ThumbHash in the middle and BlurHash on the right):

![Side by side](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ntsm5v6i2d5mfu57uesm.png)

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](https://www.npmjs.com/package/jpeg-js) to decode the image:

```ts
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
```

Then [pica](https://www.npmjs.com/package/pica) to resize it and a simple [function](https://github.com/brycedorn/blog/blob/master/src/utils.ts#L125) I wrote to crop it:

```ts
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)
```

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

```ts
import { thumbHashToDataURL, rgbaToThumbHash } from 'thumbhash'

const thumbhash = rgbaToThumbHash(100, 100, cropped)

return thumbHashToDataURL(thumbhash)
```

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](https://github.com/aFarkas/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:

```jsx
<img 
  class="lazyload blur-up" 
  src={post.thumbhash} 
  data-src={post.cover_image}
/>
```

Then after including the script tag in the renderer it Just Works™️! I followed the steps to add the [blur/unblur effect](https://github.com/aFarkas/lazysizes#lqipblurry-image-placeholderblur-up-image-technique) as well, as you can see from the `blur-up` class.

---

This is now active on [blog.bryce.io](https://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 😇