skip to main content
Avatar of Nicklas Jarnesjö
Nicklas Jarnesjö

Blurred image placeholder for Next.js image (next/image)

code
Blurred image as placeholder beside real image

As seen in gif above (a bit distorted) is what I have accomplished described in the post below. And on this image below you can see the blurred image which is a base64-encoded image loaded first and it's created from the image to the right with a webpack-loader.

Blurred image as placeholder beside real image

UPDATE
It was initial written about lqip-loader but after some npm vulnerabilities and lack of support and decided to rewrite it and luckily found lqip-modern-loader. So updated content follows


When I started using Next.js for the first time for real were just in break between version 9 and 10. So when I heard they should include a new optimized image component I were excited.

It's nice when the framework adds things like this which is build together with framework and optimized for it. But as in many cases it could missing features you thought would be nice to have. One of those things are some sort of placeholder for the image before it's loads because of the lazy loading.

I have experiment with a lot of other npm packages such as next-optimized-images and got to know lqip for the first time. So why didn't I went that way? It have it's pros and cons as next/image and I want to stick to it and see how it evolves and in my opinion feels a bit more future-proof.

The solution

So the solution were that it implemented a webpack loader which gives me a base64 string of the image I can add into a wrapper together with next/image which is absolute positioned and when the image loads it will appear above the placeholder and you will get the illusion they are stitched together.

npm install lqip-modern-loader --save-dev

Add the lqip-modern-loader into the next.config.js and it will provided you with the base64 image which we uses in our custom image component.

next.config.js
module.exports = {
  webpack: (config, options) => {
    config.module.rules.push({
      test: /\.(gif|png|webp|jpe?g)$/i,
      use: [
        {
          loader: 'lqip-modern-loader'
        }
      ]
    })
    return config
  }
}

We still using the Next.js pattern to store our images into the public-directory and when we're doing the required we are loading it through our newly added webpack loader.

CustomImage.tsx
import Image, {ImageProps} from 'next/image'

const CustomImage = ({src, ...props}: ImageProps) => {
  const image = require(`../public${src}?lqip`)

  return (
    <div className="relative overflow-hidden flex rounded-none sm:rounded-md">
        <img
          src={image.dataURI}
          className="absolute inset-0 w-full h-full transform scale-110 m-0"
          style={{filter: 'blur(20px)'}}
          aria-hidden="true"
          alt=""
        />
      <Image src={image.src} {...props} />
    </div>
  )
}

export {CustomImage}

Here you can see an example of what you will get back from const image = src.startsWith('http') ? {src} : require(`../public${src}?lqip`)

// const image = src.startsWith('http') ? {src} : require(`../public${src}?lqip`)
// console.log(image)
{
  "src": "/_next/static/media/home-office.0484ab3be39e7d374d77eea012746d12.jpg",
  "width": 24,
  "height": 18,
  "dataURI": "data:image/jpg;base64,/9j/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAASABgDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAgJCgYH/8QAJxAAAQUBAAIBBAIDAQAAAAAAAwECBAUGBwAIEQkSExQVMRYYJTL/xAAXAQADAQAAAAAAAAAAAAAAAAAEBQYH/8QAKhEAAwABAgMFCQAAAAAAAAAAAQIDBAARBRITBjNyc7EUISIkQkNRssH/2gAMAwEAAhEDEQA/AKW+C8s93vaXq1bG41yN3U+fR+aUWrk0+FrGzdcDPTayPGj6C3A+UaWCazQMLCd9z4tUaKUZFEE5EYOfvoP6wV3uh2bQcdW+jc9uMzSZ+xLZmgzOgfmtNDro2PjVEulyDm2FHEhWMlXW2lsyJS1j2Miy3Cecb0pbxXsp3TgPNuS2fP8AoNvy+p2VJL5tYbet1Gtzv8a11lMlfrapmOmRbK4yTTxLpo4oRq6vPUDmi/JIGiOt14T9Q33d59iq6FyfC/T56rZc4y9JHl6HDy8yzWWOOkn/AIxpt5+pFo93oDXKoqFsrXWOkyLxG2kxHTWCRFVMvtA2RxJevhyxGMhwm8S/tUSJwGS2clyYUWeQLiYgyfL0jzIKo70qzxUoMWmNl0LBnrlxtFaBwygzlj1Ioy/lVMQikMDR9yFlP7Heq3+s3Uthyo95B0snGWI6mRe10WTBhTyvgQpz3giSyFOBA/uNC5rykVSic5r1arV8ecz3b2Z2nZ87Xb7rfJMpwTY02Tubfd0HNlu7L/qo+wSsnzbnQ3F5HuySWxoLXPAR6RRyVZIG9RoTx5V4HHFxcLEnnE2yhGYtWfS5K1VVWlAA4ADuCwA93KQBsNWsO02Hi4+KuVuavCVGZOlsSQoYlUcBGLKxMyAyH4SoI21lq7Gc7uE44LjFcEN9JUInEeoxKtvrCKomK77RqrzFev2ony8pHf29yr719MBV/wA07eT5X8ichhNR/wAr96Nd0XGI5Ed/6RHIiIqfPwvwnz/SePHiQ9zl+bT0TWTL30fBP1bWiDusmTJveoVsiQc9cfC7gR4BikLCMOMHmbo4yxXucAjI7pUlwWPGrRLIOo0apifc8ePEzfT4R/dGW+z5S/vr/9k="
}

And an example of the CustomImage component in use. Still needing every necessary properties as next/image

<CustomImage
  src="/static/images/home-office.jpeg"
  alt="My computer desk at home"
  width="4032"
  height="3024"
/>

Caveats

One problem with this solution are that it's only supports jpg and png at the moment. And to have fallback solutions is to add another webpack loader file-loader.

npm install file-loader

We then update next.config.js with the following code

next.config.js
module.exports = {
  webpack: (config, options) => {
    config.module.rules.push({
      test: /\.(gif|png|webp|jpe?g)$/i,
      use: [
        {
          loader: 'lqip-modern-loader'
        }
      ]
    })

    config.module.rules.push({
      test: /\.(svg|png|jpe?g|gif|webp|mp4)$/i,
      use: [
        {
          loader: 'file-loader',
          options: {
            publicPath: '/_next',
            name: 'static/media/[name].[hash].[ext]'
          }
        }
      ]
    })

    return config
  }
}

Update the CustomImage-component to support fallback source for not supported image types.

CustomImage.tsx
import Image, {ImageProps} from 'next/image'

export type CustomImageType = ImageProps & {
  noPlaceholder?: boolean
}

const CustomImage = ({src, noPlaceholder, ...props}: CustomImageType) => {
  const image = src.startsWith('http') ? {src} : require(`../public${src}?lqip`)

  return (
    <div className="relative overflow-hidden flex rounded-none sm:rounded-md">
      {image.dataURI && !noPlaceholder && (
        <img
          src={image.dataURI}
          className="absolute inset-0 w-full h-full transform scale-110 m-0"
          style={{filter: 'blur(20px)'}}
          aria-hidden="true"
          alt=""
        />
      )}
      <Image src={image?.src || image.default} {...props} />
    </div>
  )
}

export {CustomImage}

As you seen above I have added support for external images and also added the property noPlaceholder if you have transparent images and it be a bit weird when you get a blurred image behind it. So with new prop you have opt-out the placeholder.

Hope you find this useful and if you have any questions or improvements suggestion just reach out to me on Twitter @jarnesjo

Discuss this post on Twitter
Categorycode