DEV Community

Cover image for How to create and serve image blur placeholders
Jake Holman
Jake Holman

Posted on • Originally published at jakeisonline.com

How to create and serve image blur placeholders

๐Ÿ“Œ Originally posted with live interactive demos on jakeisonline.com

Imagine a page with a large grid of images, you don't want your user staring at blank spaces slowly being filled in as the images load. Gross. Instead, give them a quick visual indication that something is loading, with a hint of what's to come.

Thanks to Karsten Winegeart from Unsplash for the amazing pups
Thanks to Karsten Winegeart from Unsplash for the amazing pups

If you're not familiar with blur placeholders, it's a technique of creating a blurred version of the original image at a fraction of the file size, and then encoding that blurred version directly into the HTML. The blurred image loads as immediately as the page, whilst the original image is swapped in later.

This technique allows for a really fast, optimised initial loading experience for visitors with any level of internet connection. Those with a fast connection likely won't even see the blurred image, whilst those on slower connections won't be jarred by a sudden blank space being filled in.

Creating the blurred image

Partly due to a lack of shame, we'll use my face from here on out.

In order to create this blurred image, we'll need to perform two steps:

  1. We should resize the original image to be much smaller than the original. Not only will this of reduce the final size of the blurred image, it will keep the encoding performant. If you care about neither of these things, you can skip this step.
  2. We're going to need to create a hash of the blurred image (known as a "blurhash"), which we'll later convert to a base64 image data string to serve to the client.

Resizing the image

In the interests of performance and reducing the amount of data we need to store, we'll want to resize the image to be as small as possible.

Because we're going to be blurring the image, it means we can resize and store a very tiny thumbnail of the image, and then upscale to the desired size when rendering it. The blurring that the blurhash library performs will mean that the upscaled image will look great, and the user won't be able to tell it's being upscaled.

Doing this in Node is simple, we'll use the popular sharp library to resize the image.

npm install sharp
Enter fullscreen mode Exit fullscreen mode

sharp gives us a powerful, performant, and easy way to do all sorts of image operations. In this case, we're going to be using it to resize the image to be much smaller than the original.

import path from "path"
import fs from "fs"
import sharp from "sharp"

// Resolve the path to the image
const filePath = path.resolve(process.cwd(), "path/to/image.jpg")

// Read the image file into a buffer
const imageBuffer = fs.readFileSync(filePath)

// Calculate optimal dimensions for blurhash
const aspectRatio = Number(width) / Number(height)
const minDimension = 32

// Calculate dimensions ensuring minimum of 32 pixels on
// the smaller side
let blurWidth, blurHeight
if (aspectRatio >= 1) {
  // Landscape or square
  blurHeight = minDimension
  blurWidth = Math.round(minDimension * aspectRatio)
} else {
  // Portrait
  blurWidth = minDimension
  blurHeight = Math.round(minDimension / aspectRatio)
}

// Give sharp the buffer, and then call the resize method,
// and make sure to return that data as raw bytes
const { data: imageData, info: imageMeta } = await sharp(imageBuffer)
  .resize(blurWidth, blurHeight, { fit: "inside" })
  .ensureAlpha()
  .raw()
  .toBuffer({ resolveWithObject: true })

// We'll use the raw bytes in a bit
Enter fullscreen mode Exit fullscreen mode

But wait, won't resizing the image to be smaller result in a loss of quality? Not really. To illustrate both the resized image and the results of upscaling with blurhash, take a look at the following example:

Running blurhash on the smaller image and then upscaling it shows almost no loss of quality.<br>

We can resize the original image to be much smaller, blurhash it, and then upscale to the desired size when rendering it. Almost no detail it lost despite the upscaling.

Creating the blurhash

Once we have our image in the desired size, we'll use a fantastic library called blurhash to create a hash of the blurred image (a string of seemingly random characters that represents the image). This hash can then be stored, and later decoded to get the image data.

npm install blurhash
Enter fullscreen mode Exit fullscreen mode

With blurhash installed, we can create a hash from the resized image data we generated earlier:

// Create a new array with the correct format
const rgbaData = new Uint8ClampedArray(imageMeta.width * imageMeta.height * 4)

for (let i = 0; i < imageData.length; i++) {
  rgbaData[i] = imageData[i]
}

const hash = encode(rgbaData, info.width, info.height, 5, 4)
Enter fullscreen mode Exit fullscreen mode

Wait, what's all this extra code about? While sharp returns the image data in a format, blurhash expects the image data to be in a specific format called Uint8ClampedArray.

So we simply need to create a new array (rgbaData) that's exactly the right size for the image data (width x height x 4, where the 4 represents the RGBA channels - Red, Green, Blue, and Alpha/transparency), and then copy over all the pixel data from the original format to this new format.

We end up with a hash that looks something like this:

-VHd]Lt7TH.7xuR+.le?t5xu%2t6OZxar?S4XSoy%MOTV[spoMn,%MV@aeafjuWqkqsUNaWBt6WXogtPafjut6ofs;oea#kBoeof
Enter fullscreen mode Exit fullscreen mode

That's my face at 32x32, blurred, and represented as a string. You're welcome!

Controlling the blur

Blurhash gives us two parameters to control the blur, called componentX and componentY. Roughly, componentX controls the horizontal blur, and componentY controls the vertical blur.

๐Ÿ“Œ See an interactive blur control on jakeisonline.com

Personally, I've found that componentX = 5 and componentY = 4 gives a good balance between blur and detail, but you should play around with the values to get your desired sweet spot.

Note though, "The more components you pick, the more information is retained in the placeholder, but the longer the BlurHash string will be", so you'll need to balance that with the amount of detail you want to retain.

Save the blurred placeholder

Now that you have your blurhash string, you'll need to store it somewhere. Where you store the blurhash is entirely up to you. You could store it in the database, or in a file, or in a cache, or wherever you want.

So long as you can quickly retrieve for decoding and server side rendering later.

Why not just store the base64 encoded image data?

We could at this point decode the blurhash into a base64 encoded image data string, and store that in the database.

This would mean no decoding is required to display the image, but at the expense of increased storage requirements.

In my example here, the base64 encoded image data is significantly larger than the blurhash string. My example image is ~5,500 bytes in string length as base64, but only 100 bytes as the blurhash string. That's a 190%+ difference!

Displaying the blurred placeholder

Once we have our image resized, compressed into a blurhash, and then stored somewhere, we'll inevitably want to display it somewhere.

To do that, we'll need to:

  1. Decode the blurhash from a string to an array of raw pixel data
  2. Convert the decoded pixel data to an image format, generally a PNG
  3. Convert that image into a base64 encoded string

Thankfully we have a library that can do all of this for us: blurhash-base64.

npm install blurhash-base64
Enter fullscreen mode Exit fullscreen mode

Now all we need to do is decode the blurhash:

const hashBase64 = await blurhashToBase64(hash)
Enter fullscreen mode Exit fullscreen mode

And then we can display the image!

<img src="{hashBase64}" />
Enter fullscreen mode Exit fullscreen mode

blurhash-base64 gives us a base64 encoded image data string that we can use to display the image.

Why not just use CSS?

Rather than doing all this fussy stuff with encoding and decoding, you could simply stretch the resized image to the desired size, use CSS to apply blur to it, and then wrap it in a container with the same size as the original with overflow-hidden to ensure the edges are crisp (CSS blur feathers anything it is applied to).

Showing the difference of CSS only and the fullstack blurhash approach

While this would result in a smaller file size for the client, there are a few downsides:

  1. You'll need to store that image data somewhere, in order to include it in the initial HTML
  2. Storing the image data will result in significantly larger storage requirements. My example here would result in needing to store ~940 B vs only 100 B for the blurhash, and that's with a relatively small image.
  3. Now you've got to mess with markups and CSS to get the desired effect, and you'll still need JavaScript to swap the placeholder for the original image.
  4. I personally feel the CSS version looks a bit meh, like I'm looking through a dirty smeared lense instead of the frosted glass effect of blurhash.
  5. The original pixelated image is still there, and you're at the mercy of the browser's rendering engine to decide how good the blur effect is going to be.

Even without file storage concerns, I think the blurhash version is the better option.

Swapping the placeholder for the original image

Now that we have our blurred placeholder, and we're rendering it on the server, we'll need to swap it for the original (non-blurred) image when it's loaded on the client.

Executing the following script in the client will swap the placeholder for the original image only when the image is within 50px of the viewport.

// Select all images with a data-original-src attribute
const blurredImages = document.querySelectorAll("img[data-original-src]")

// Create an observer to load the images when they're in the viewport
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        loadHighQualityImage(entry.target)
      }
    })
  },
  {
    // Begin loading image when it's within 50px of
    // the viewport for perceived performance
    rootMargin: "50px",
  },
)

const loadHighQualityImage = (img) => {
  // Disconnect observer to prevent any accidental
  // loading due to erratic scrolling
  observer.disconnect()

  // Set the src directly, which will trigger browser
  // to load the image
  img.src = img.dataset.originalSrc

  // May as well clean up after ourselves
  delete img.dataset.originalSrc
}

// Begin observing all images with a data-original-src attribute
blurredImages.forEach((img) => {
  observer.observe(img)
})
Enter fullscreen mode Exit fullscreen mode

And we're done! Now you can take any image, resize it, blur it, save it, and then display it on your page with a placeholder that's instantly rendered for users, and then swapped for the original image when it's loaded.

Top comments (0)