Implementing Dynamic Image Placeholders in Next.js

Notes from studying image behavior in Gatsby and Discord
Overview

Discord image placeholder
While using Discord, I noticed an interesting detail.
It improved the user experience by showing a dynamic placeholder in the image area, and I wanted to apply the same idea to my blog.
If I had built the blog with Gatsby, I could have used the gatsby-image plugin to get this behavior automatically.
But I wanted to implement it myself in Next.js using the development knowledge I had.
This post shares the process of using Node.js to generate blur data from images automatically.
Design
loading = "lazy"; // {lazy} | {eager}
blurDataUrl = "~~"; // Only base64-encoded images are supportedNext's Image component has loading and blurDataUrl props.
Next.js already provides the lazy loading behavior and the placeholder behavior I wanted.
The remaining task was to get the dynamic base64 data needed for blurDataUrl.
I wanted a design that used the strengths of Server Components, where logic runs on the server and users receive only the resulting UI.
I also wanted to keep the benefits of static export, or SSG.
That meant I needed a JavaScript script that could do the following:
- Access the
imgsfolder. - Generate base64 data for each image.
- Save the generated base64 data as JSON so components can reference it statically.
To do this, I used the fs library, which can access the file system in a Node.js environment, and the sharp library for image processing.
For dynamic images, you must provide the blurDataURL property. Solutions such as Plaiceholder can help with base64 generation.
Next.js docs
The Next.js Image docs recommend Plaiceholder when you want dynamic blur data.
Using Plaiceholder also requires installing sharp.
I am sure the library authors released it after thinking through many cases, but if I had to use sharp anyway, I wanted to design the workflow myself and learn through the mistakes.
Implementation
Dependencies
- next.js@14.1.0
- sharp@0.33.2
- To use the latest version of
sharp, you need to update Next.js to 14.1.0 or later to avoid errors in the Vercel deployment environment. Next.js 14.1- This uses a Node.js library, so it only works in Server Components.
- The design is based on Next.js App Router static export.
Generating dynamic blur data
The code below may be limited or may not work in a browser environment.
// generateBase64Image.mjs
import { readdir, readFile, writeFile, mkdir } from "fs/promises";
import sharp from "sharp";
const generateAllBase64 = async (dir) => {
// 1. Access files
const imageDir = path.join(process.cwd(), "public", "imgs", dir);
let result = {};
// 2. Filter image files
let files = await readdir(path.join(imageDir));
files = files.filter((file) => {
return (
file.endsWith(".jpg") ||
file.endsWith(".jpeg") ||
file.endsWith(".png") ||
file.endsWith(".gif") ||
file.endsWith(".webp")
);
});
// 3. Read images and convert them to base64
for (const filename of files) {
const imageBuffer = await readFile(
path.join("public", "imgs", dir, filename)
);
const base64 = await sharp(imageBuffer)
.resize(10, 5)
.toBuffer()
.then((buffer) => {
const base64Image = `data:image/jpeg;base64,${buffer.toString(
"base64"
)}`;
return base64Image;
})
.catch((err) => console.error(err));
// 4. Build the result object
result[filename] = {
base64,
img: { src: `/imgs/${dir}/${filename}` },
};
}
// 5. Return the result object
return result;
};This code accesses my imgs folder inside public, reads the image files, and converts them to base64.
Following the Next.js recommendation, the resized image should stay under 10px.
Automation
// generateBase64Image.mjs
const init = async () => {
// 1. Read folder names from a specific imgs directory
const dirNames = getDirNames();
let result = {};
// 2. Convert images to base64 for each folder
for (const dir of dirNames) {
const base64Data = await generateAllBase64(dir);
result[dir] = base64Data;
}
// 3. Save base64 JSON files
for (const dir in result) {
await writeJSON(result[dir], dir);
}
};
init();The script scans the imgs folder and saves a JSON file for each folder name.
If this JavaScript file is run before build through a package script, it works in production and deployment environments as well.
Closing

Applied result
This task involved many rounds of trial and error.
At first, I saved the data as a txt file instead of JSON and caused errors in production and deployment environments.
I also spent a long time trying to understand a version conflict between Next.js and sharp.
That made me realize why --legacy-peer-deps can be dangerous.
Compared with my implementation, which was tightly scoped to my blog environment, Plaiceholder has gone through far more abstraction so that many people can use it.
That felt impressive.
I also learned that Plaiceholder generates base64 data on every request when the environment is not static export.
I have never contributed to open source before, but I felt tempted to solve that part and contribute to the library.