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 aBuffer
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 usingbuffer.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 thepublic
directory and the provided file path. Then, it reads the file asynchronously usingfs.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 usinggetFileBufferLocal()
. Then, it usessharp
to resize the image to a width of 20 pixels. The resized image buffer is then converted to base64 format usingbufferToBase64()
. 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 abuffer
from a remote URL using thefetch API
. This function fetches the remote file and converts the response to a buffer usingarrayBuffer()
.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 withhttp
, it is considered a remote file, andgetFileBufferRemote
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
source code: https://github.com/dpnunez/nextjs-image-blur-with-sharp
live example: https://nextjs-image-loading.vercel.app/
Top comments (4)
can you share any way to implement this in client-side rendering?
You are a lifesaver!
Hey, that's a good article! Why are the source code and demo not available?
Hey, I'm glad you liked the article! I ended up forgetting to update the final repository url, fixed, thanks for letting me know!