The Nextjs Image Component

The Nextjs Image Component is awesome. It does a lot of magic. To leverage the full magic of the image component it is required to import the image that needs to be enchanted.

import PictureOfMeOnTheBeach from "public/PictureOfMeOnTheBeach.png"

Now Nextjs will do the following:

  • Get the actual width and height of the image and provide it to the img element.
  • Generate a blurDataURL and show it until the actual image is loaded.

As stated above this only works if you import the image. I know two cases where you aren't able to do this:

  1. Dynamically loaded: If you don't know the exact images at build time.
  2. Remote image: If you retrieve the image from a remote location.

I recently experienced the first problem and here is how I dealt with it.

Part One: Dynamically load images

Let's start by loading all images within a folder:

const imagesDirectory = path.join(
  process.cwd(),
  `public/products/${product.id}`
)

// Wrap the readdir in a try-catch block, otherwise the build will fail because of a unhandled exception.
try {
  const productImagePaths = await fs.readdir(imagesDirectory)

  return {
    props: {
      images: productImagePaths.map((path, index) => ({
        path: `/products/${product.id}/${path}`,
      })),
    },
  }
} catch (error) {
  console.warn(
    `Image ${product.name} has no images under /public/product/[id]!`
  )
  return {
    props: {
      images: [],
  }
}

Part Two: Generate a BlueDataURL

The Nextjs Image Component provides a prop called blurDataURL to show a very blurry version of the image while the actual image is loading. To generate this URL I use Plaiceholder:

const imagesDirectory = path.join(
  process.cwd(),
  `public/products/${product.id}`
)

// Wrap the readdir in a try-catch block, otherwise the build will fail because of a unhandled exception.
try {
  const productImagePaths = await fs.readdir(imagesDirectory)

  /**
   * Create blurDataURLs (base64) as image placeholders
   */

  const blurDataURLs = await Promise.all(
    productImagePaths.map(async src => {
      const { base64 } = await getPlaiceholder(`/products/${product.id}/${src}`)
      return base64
    })
  ).then(values => values)

  return {
    props: {
      images: productImagePaths.map((path, index) => ({
        path: `/products/${product.id}/${path}`,
        blurDataURL: blurDataURLs[index],
      })),
    },
  }
} catch (error) {
  console.warn(
    `Image ${product.name} has no images under /public/product/[id]!`
  )
  return {
    props: {
      images: [],
    },
  }
}

Part Three: Make it look sexy

Now it's time to use the image source and blurDataURL with the Nextjs image component.

// Use whichever styling solution you want. But it is important to add 'transition' to the Image component
const AnimatedImage = styled(Image, {
  transition: ".3s",
});

const ProductPage = ({ images }) => {
      <ImageContainer>
        {images.length ? (
          <AnimatedImage
            src={images[0].path}
            layout="fill"
            objectFit="cover"
            alt={images[0].path}
            placeholder="blur"
            blurDataURL={images[0].blurDataURL}
          />
        ) : (
          <Image
            src={PlaceholderImage}
            layout="fill"
            objectFit="cover"
            alt="placeholder"
          />
        )}
      </ImageContainer>
  );
};

The important parts are:

  • placeholder need to be blur
  • blurDataURL needs to be the base64 string generated by plaiceholder

I use layout="fill", that's why the actual image size is not that important to me. If you need this data you can use sharp's metadata for this.