How to: Blurred images on load in Next.js

Next.js' <Image> component is a really impressive piece of engineering. It offers automatic image optimisation, all the SEO features you'd want from html image tags and some more arcane properties like blurDataURL, which is supposed to help Next.js display a low-res blurred image before loading its full-res version.

thumbnail with the text "Blurring images on load in Next.js and Node.js"

If that sounds too good to be true, well that's probably because it is. The Next.js documentation is very vague about the blurDataURL property.

Data URI to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".

Must be a base64-encoded image. It will be enlarged and blurred, so a very small image (10px or less) is recommended. Including larger images as placeholders may harm your application performance

...Thanks?

I don't know about you but the above means absolutely nothing to me in terms of actually using it. It took me a while in fact to figure out how to implement it, so here's everything I've learned about how to blur images in Next.js.

What is the blurDataURL property for the Image component?

The Next.js docs give us a little information, despite being a bit confusing:

  • Must be a base64-encoded image
  • a very small image (10px or less)

This means we need to generate a base64-encoded version of our image that is 10px or less. Pretty easy right? No, not really, but luckily there's a package for that!

Generating a base64 encoded image for blurDataURL

Plaiceholder is going to be our package of choice for this. It's a nifty utility that can be used with Next.js or Node to generate the information we need. On top of blurDataURL we'll also need to know the image's size, which we can get with image-size

npm i sharp plaiceholder image-size
# or
yarn add sharp plaiceholder image-size

For this article we'll use a lightly altered (and non-typescript) way of getting the information we need originally written by Nikolov Lazar in his blog Generating blurDataURL for remote images in Next.js.

The code we use here is mostly the same, but I'd like to show off a couple of different ways to use it in case you're not working with MDX.

/**
 * utils/imageMetadata.js
 * Code written by Nikolov Lazar 
 * https://nikolovlazar.com/blog/generating-blur-for-dynamic-images-nextjs
 */

import imageSize from 'image-size'
import path from 'path'
import { getPlaiceholder } from 'plaiceholder'
import visit from 'unist-util-visit'
import { promisify } from 'util'

// Convert the imageSize method from callback-based to a Promise-based
// promisify is a built-in nodejs utility function btw
const sizeOf = promisify(imageSize)

// Just to check if the node is an image node
function isImageNode(node) {
    const img = node
    return (
        img.type === 'element' &&
        img.tagName === 'img' &&
        img.properties &&
        typeof img.properties.src === 'string'
    )
}

// Returns the props of given `src` to use for blurred images
export async function returnProps(src) {
    // Calculate image resolution (width, height)
    const res = await sizeOf(path.join(process.cwd(), 'public', src))
    // Calculate base64 for the blur
    const { base64: blurDataURL, img } = await getPlaiceholder(src)

    // If an error happened calculating the resolution, throw an error
    if (!res) throw Error(`Invalid image with src "${node.properties.src}"`)

    const { width, height } = res

    return {
        ...img,
        width,
        height,
        blurDataURL,
    }
}

async function addProps(node) {
    // return the new props we'll need for our image
    const { width, height, blurDataURL } = await returnProps(
        node.properties.src
    )

    // add the props in the properties object of the node
    // the properties object later gets transformed as props
    node.properties.width = width
    node.properties.height = height

    node.properties.blurDataURL = blurDataURL
    node.properties.placeholder = 'blur'
}

const imageMetadata = () => {
    return async function transformer(tree) {
        // Create an array to hold all of the images from the markdown file
        const images = []

        visit(tree, 'element', (node) => {
            // Visit every node in the tree, check if it's an image and push it in the images array
            if (isImageNode(node)) {
                images.push(node)
            }
        })

        for (const image of images) {
            // Loop through all of the images and add their props
            await addProps(image)
        }

        return tree
    }
}

export default imageMetadata

For this I've specifically separated some of the logic into it's own function returnProps, which we'll use when working with more standard APIs.

How to blur images in Next.js from a Node.js API

This is probably the easiest way to return the information you need to blur images. Whenever an image needs to be returned by an endpoint, you'll also have to make sure to use plaiceholder to return its data. It should look something like this:

import { returnProps } from 'utils/imageMetaData'

/**
 * Random example using Next.js API functions, use whatever Node backend you
 * prefer
 */
export default function handler(req, res) {
  // Get your data from a database or any other source
  const data = getPostsFromDatabase();
  
  // Pass the image to plaiceholder
  const imageProps = await returnProps(data.thumbnailSrc);
  
  // Or maybe you have a gallery
  data.gallery = await Promise.all(
    data.gallery.map(async item => {
      const imageProps = await returnProps(item.imageSrc);
      
      // This will return the image a well as the needed plaiceholder
      // info in the same object within the array 🤯
      return { ...item, imageProps };
    })
  );
  
  res.status(200).json({ 
    ...data,
    // This data will then be used by <Image> in our frontEnd
    thumbnail: imageProps
  })
}

How to blur images in a statically exported Next.js site

Using plaiceholder in a statically generated Next.js site is also pretty easy.

export const getStaticProps = async context => {
  // Get your data from a database or any other source
  const data = getPostsFromDatabase();
  
  // Pass the image to plaiceholder
  const imageProps = await returnProps(data.thumbnailSrc);
  
  // Or maybe you have a gallery
  data.gallery = await Promise.all(
    data.gallery.map(async item => {
      const imageProps = await returnProps(item.imageSrc);
      
      // This will return the image a well as the needed plaiceholder
      // info in the same object within the array 🤯
      return { ...item, imageProps };
    })
  );
  
  return {
    props: {
      data,
      // This data will then be used by <Image> in our frontEnd
      thumbnail: imageProps
    },
  };
}

How to blur images in Next.js from a markdown file

Blurring images within Markdown or MDX is thankfully a lot easier thanks to the rest of Nikolov's functions. We just need to pass the plugin to our serialize function.

import imageMetaData from 'utils/imageMetadata'
import { serialize } from 'next-mdx-remote/serialize'


const components = {
    img: (props: any): React.ReactNode => (
        <Image {...props} layout="responsive" loading="lazy" />
    ),
}


function Post({ mdx }) {
    return (
        <>
            <MDXRemote
                {...mdx}
                components={components}
            />
        </>
}

export const getStaticProps = async context => {
  // Get your data from a database or any other source
  const markdown = getPostsFromDatabase();
  
  // convert your markdown to html with unified, rehype or remark
  const mdx = await serialize(markdown, {
      mdxOptions: {
          rehypePlugins: [imageMetaData],
      },
  })
  
  return {
    props: {
      mdx,
    },
  };
}

That's all! Try to load your page. Your images should have a beautiful blur before they lazy-load into view.

Leave a comment (Powered by Commentcarp)