Next.js Image Loading with Blur Effect: A Step-by-Step Guide

Next.js Image Loading with Blur Effect: A Step-by-Step Guide

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.

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.

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

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.

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 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

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

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

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:

bufferToBase64 function: 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’).

getFileBufferLocal function: 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.

getPlaceholderImage function: 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.

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

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.

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

Functionality for Remote Files: 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().

Unified Buffer Retrieval: 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

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

Resources

code source: https://github.com/dpnunez/nextjs-image-loading
live example: https://nextjs-image-loading.vercel.app/

Leave a Reply

Your email address will not be published. Required fields are marked *