Next.js Image Loading with Blur Effect: A Step-by-Step Guide
Tags: webdev, nextjs, javascript, react
Published: 2024-04-08 03:11:28

During the development portfolio's front end, I came across a performance problem that involved **image loading**. 

If you're already using Next.js, you've likely discovered that the optimal method for rendering images in the app is by utilizing the Image component provided by Next.js. The advantages of this component are significant, including preventing layout blinking during or after loading, intelligent resizing to decrease image loading times, and more.

In this article, I'll delve into implementing the loader effect using blurred data, a feature native to the Image component. However, it can be a bit tricky to utilize.

---

# Problem Statement
I encounter a challenge when dealing with heavy images on a webpage, especially in cases of low internet speed. I aim to avoid rendering a blank space while the client is downloading the image file. Instead, I prefer to display a loader, particularly a blurred one, during this process.


![problem gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8pizhn9fjistlq9txwjl.gif)

---

# How to test
If you're using Chrome or any Chromium-based browser, you can simulate slow internet connections by accessing the "Network" tab in the developer tools. This feature is likely available in most mainstream browsers and can help replicate conditions of slow internet speeds for testing purposes.


![How to simulate network issues](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jk3fbebz7t28e2dbmwbi.png)

---

# Splitting the solution
The best way to think about how to solve this problem is to split the solution into 2 sides

- `static` image render
- `dynamic` image render


The way that Next.js handles these cases is significantly different, let's start with the simplest, static image render.

---

## Static image

> See the [commit changes](https://github.com/dpnunez/nextjs-image-blur-with-sharp/commit/65d6b9bf487f108e2b89a82239133a55a43e4e2f)

In this case, Next.js takes care of all the hard work for us. During build time, Next.js will recognize that we're importing the image in a component/page and generate the blurred version automatically. Our only task here is to provide the `placeholder` prop with the value `blur`.

```typescript
import Image from 'next/image'
import image1 from '../../../../public/images/photo-1.jpeg'
import image2 from '../../../../public/images/photo-2.jpeg'
import image3 from '../../../../public/images/photo-3.jpeg'

export default function Page() {
  return (
    <>
      <Image className="image-style" src={image1} placeholder="blur" alt="" />
      <Image className="image-style" src={image2} placeholder="blur" alt="" />
      <Image className="image-style" src={image3} placeholder="blur" alt="" />
    </>
  )
}

```
---
## Dynamic images


Now we face a challenging scenario because Next.js, during build time, cannot anticipate which images we will render and generate a blurred version accordingly. Therefore, the responsibility of generating a blurred version is addressed to us.

### Setup

> Most guides and tutorials about blurred loading, including Next.js documentation recommendations, suggest using a library called `plaiceholder`. However, I do not recommend using that library, considering it is no longer maintained and has caused build problems in the Vercel environment.

To generate the blurred version, we will utilize the [sharp](https://sharp.pixelplumbing.com/) library. This library functions by taking a `Buffer` as input and returning a new buffer containing a resized version of the image. From the `Buffer` of the resized version, we'll generate a base64 format, which is supported by `next/image`. We will delve deeper into the functionality of this library later during the implementation.


install dependencies
```bash
npm install sharp
```

### Approaches

With sharp installed, the next step is to obtain a `Buffer` containing image data, pass it to `sharp`, and handle the result with the new `Buffer`. However, obtaining a `Buffer` from a local image differs from obtaining one from a remote image. Therefore, we will split our solution into two parts: handling **local** images and handling **remote** images.

### Local image

> See the [commit changes](https://github.com/dpnunez/nextjs-image-blur-with-sharp/commit/ae37b95a3730fac1b84d9f221f779e70efb8d147)

Our goal is to create a function that receives an image path. In our implementation, this path exactly matches how you would pass it directly to the `Image` component (you should not include `public` or any relative path). The function should return a base64 data obtained by converting the `Buffer` of the resized version generated by sharp. Below is the implementation of this function:

> _For more information about how to properly read files on server side, access Vercel tutorial [How to Read files in Vercel Function](https://vercel.com/guides/how-can-i-use-files-in-serverless-functions#examples-of-reading-files)_


```typescript
'use server'
import sharp from 'sharp'
import { promises as fs } from 'fs'
import path from 'path'

function bufferToBase64(buffer: Buffer): string {
  return `data:image/png;base64,${buffer.toString('base64')}`
}

async function getFileBufferLocal(filepath: string) {
  // filepath is file addess exactly how is used in Image component (/ = public/)
  const realFilepath = path.join(process.cwd(), 'public', filepath)
  return fs.readFile(realFilepath)
}

export async function getPlaceholderImage(filepath: string) {
  try {
    const originalBuffer = await getFileBufferLocal(filepath)
    const resizedBuffer = await sharp(originalBuffer).resize(20).toBuffer()
    return {
      src: filepath,
      placeholder: bufferToBase64(resizedBuffer),
    }
  } catch {
    return {
      src: filepath,
      placeholder:
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOsa2yqBwAFCAICLICSyQAAAABJRU5ErkJggg==',
    }
  }
}

```

explanations:
- <u>`bufferToBase64` function</u>: This function takes a `Buffer` object as input and returns a base64-encoded string with a data URI prefix (`data:image/png;base64,`). It achieves this by converting the Buffer to a base64-encoded string using `buffer.toString('base64')`.


- <u>`getFileBufferLocal` function</u>: This async function takes a file path (`filepath`) as input. It constructs the real file path by joining the current working directory (`process.cwd()`) with the `public` directory and the provided file path. Then, it reads the file asynchronously using `fs.readFile()` and returns a promise that resolves to the file buffer.


- <u>`getPlaceholderImage` function</u>: This async function takes a file path (`filepath`) as input. It attempts to retrieve the file buffer using `getFileBufferLocal()`. Then, it uses `sharp` to resize the image to a width of 20 pixels. The resized image buffer is then converted to base64 format using `bufferToBase64()`. The function returns an object containing the original file path (`src`) and the base64-encoded placeholder image (`placeholder`). In case of an error (e.g., if the file cannot be read or resized), it returns a default placeholder image encoded in base64.


#### On Next Page

In the image component, beyond the `placeholder` prop, now we need to pass `blurDataURL`, which receives a `base64`.

```typescript
import { getPlaceholderImage } from '@/utils/images'
import Image from 'next/image'

const images = [
  '/images/photo-4.jpeg',
  '/images/photo-5.jpeg',
  '/images/photo-6.webp',
]

export default async function Page() {
  const imageWithPlaceholder = await Promise.all(
    images.map(async (src) => {
      const imageWithPlaceholder = await getPlaceholderImage(src)
      return imageWithPlaceholder
    }),
  )
  return imageWithPlaceholder.map((image) => (
    <Image
      className="image-grid"
      key={image.src}
      src={image.src}
      width={600}
      height={600}
      placeholder="blur"
      blurDataURL={image.placeholder}
      alt="Image"
    />{% embed 
 %}  ))
}
```

### Remote image

> See the [commit changes](https://github.com/dpnunez/nextjs-image-blur-with-sharp/commit/28425250e8f285a3fc8b4a465bed58b039a43268)

Given that you've already configured your `next.config` to support image rendering from remote hosts, the only thing left to implement is retrieving a buffer from a remote URL. For this purpose, we have the following implementation.

```typescript
'use server'
import sharp from 'sharp'
import { promises as fs } from 'fs'
import path from 'path'

function bufferToBase64(buffer: Buffer): string {
  return `data:image/png;base64,${buffer.toString('base64')}`
}

async function getFileBufferLocal(filepath: string) {
  // filepath is file addess exactly how is used in Image component (/ = public/)
  const realFilepath = path.join(process.cwd(), 'public', filepath)
  return fs.readFile(realFilepath)
}

async function getFileBufferRemote(url: string) {
  const response = await fetch(url)
  return Buffer.from(await response.arrayBuffer())
}

function getFileBuffer(src: string) {
  const isRemote = src.startsWith('http')
  return isRemote ? getFileBufferRemote(src) : getFileBufferLocal(src)
}

export async function getPlaceholderImage(filepath: string) {
  try {
    const originalBuffer = await getFileBuffer(filepath)
    const resizedBuffer = await sharp(originalBuffer).resize(20).toBuffer()
    return {
      src: filepath,
      placeholder: bufferToBase64(resizedBuffer),
    }
  } catch {
    return {
      src: filepath,
      placeholder:
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOsa2yqBwAFCAICLICSyQAAAABJRU5ErkJggg==',
    }
  }
}
```
### Differences 
- <u>**Functionality for Remote Files**</u>: In the above code, a new function `getFileBufferRemote` is added to retrieve a `buffer` from a remote URL using the `fetch API`. This function fetches the remote file and converts the response to a buffer using `arrayBuffer()`.


- **<u>Unified Buffer Retrieval</u>**: The `getFileBuffer` function is modified to determine whether the file is local or remote based on the URL. If the URL starts with `http`, it is considered a remote file, and `getFileBufferRemote` is called. Otherwise, `getFileBufferLocal` is called to handle local files.

These changes enable the `getPlaceholderImage` function to handle both local and remote file paths seamlessly, providing a consistent interface for generating placeholder images.

#### On Next Page

```typescript
import { getPlaceholderImage } from '@/utils/images'
import Image from 'next/image'

const images = [
  'https://images.unsplash.com/photo-1705615791178-d32cc2cdcd9c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTcxMjM2NzUwNA&ixlib=rb-4.0.3&q=80&w=1080',
  'https://images.unsplash.com/photo-1498751041763-40284fe1eb66?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTcxMjM2NzUxNg&ixlib=rb-4.0.3&q=80&w=1080',
  'https://images.unsplash.com/photo-1709589865176-7c6ede164354?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTcxMjM2NzUyNg&ixlib=rb-4.0.3&q=80&w=1080',
]

export default async function Page() {
  const imageWithPlaceholder = await Promise.all(
    images.map(async (src) => {
      const imageWithPlaceholder = await getPlaceholderImage(src)
      return imageWithPlaceholder
    }),
  )

  return imageWithPlaceholder.map((image) => (
    <Image
      key={image.src}
      className="image-grid"
      src={image.src}
      width={600}
      height={600}
      placeholder="blur"
      blurDataURL={image.placeholder}
      alt="Image"
    />
  ))
}
```


## Result

![solution gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5yfcrvavd38j6r0ggo0d.gif)


## Resources
source code: https://github.com/dpnunez/nextjs-image-blur-with-sharp

live example: https://nextjs-image-loading.vercel.app/