Generate Base64 Blur Placeholders for Images in Next.js

Dia Loghmari

Dia Loghmari / January 04, 2026

With Next.js and React Server Components, we can generate blur placeholders for images at build time and cache them efficiently using the plaiceholder library.

Installation

npm install plaiceholder sharp
npm install plaiceholder sharp

Implementation

Here's a simple example using Cloudinary images:

import { getPlaiceholder } from "plaiceholder";
import Image from "next/image";

async function getImageWithPlaceholder(src: string) {
const buffer = await fetch(src, {
cache: 'force-cache', // Cache the image fetch
}).then(async (res) =>
Buffer.from(await res.arrayBuffer())
);

const { base64 } = await getPlaiceholder(buffer);

return { src, base64 };
}

export default async function MyImage() {
const imageUrl = "https://res.cloudinary.com/dialoghmari/image/upload/v1716596284/hello_world_cover_88d282cfe2.jpg";

const { src, base64 } = await getImageWithPlaceholder(imageUrl);

return (
<Image
src={src}
alt="My image"
width={800}
height={600}
placeholder="blur"
blurDataURL={base64}
/>
);
}
import { getPlaiceholder } from "plaiceholder";
import Image from "next/image";

async function getImageWithPlaceholder(src: string) {
const buffer = await fetch(src, {
cache: 'force-cache', // Cache the image fetch
}).then(async (res) =>
Buffer.from(await res.arrayBuffer())
);

const { base64 } = await getPlaiceholder(buffer);

return { src, base64 };
}

export default async function MyImage() {
const imageUrl = "https://res.cloudinary.com/dialoghmari/image/upload/v1716596284/hello_world_cover_88d282cfe2.jpg";

const { src, base64 } = await getImageWithPlaceholder(imageUrl);

return (
<Image
src={src}
alt="My image"
width={800}
height={600}
placeholder="blur"
blurDataURL={base64}
/>
);
}

Optimization: Fetch a smaller image for placeholder

Since we only need a blurred placeholder, we don't need to fetch the full-size image. With Cloudinary, you can transform the URL to request a much smaller version while preserving aspect ratio by extracting dimensions from the URL itself:

async function getImageWithPlaceholder(src: string) {
// Extract width and height from Cloudinary URL transformations
const widthMatch = src.match(/w_(\d+)/);
const heightMatch = src.match(/h_(\d+)/);

const originalWidth = widthMatch ? parseInt(widthMatch[1]) : null;
const originalHeight = heightMatch ? parseInt(heightMatch[1]) : null;

// Calculate dimensions for placeholder (max 100px wide, preserve aspect ratio)
const placeholderWidth = 100;
let placeholderHeight: number | undefined;

if (originalWidth && originalHeight) {
// Calculate proportional height based on aspect ratio from URL
const aspectRatio = originalHeight / originalWidth;
placeholderHeight = Math.round(placeholderWidth * aspectRatio);
}

// Build transformation string
const transformations = placeholderHeight
? `w_${placeholderWidth},h_${placeholderHeight},c_fill`
: `w_${placeholderWidth}`;

// Transform Cloudinary URL to fetch a tiny version
// Example transformations:
// https://res.cloudinary.com/dialoghmari/image/upload/w_1200,h_600,c_fill/v1716596284/hello_world_cover_88d282cfe2.jpg
// becomes: https://res.cloudinary.com/dialoghmari/image/upload/w_100,h_50,c_fill/v1716596284/hello_world_cover_88d282cfe2.jpg

const placeholderUrl = src.includes('/upload/')
? src.replace(/\/upload\/(?:.*?\/)?/, `/upload/${transformations}/`)
: src.replace('/upload/', `/upload/${transformations}/`);

const buffer = await fetch(placeholderUrl, {
cache: 'force-cache', // Cache the image fetch
}).then(async (res) =>
Buffer.from(await res.arrayBuffer())
);

const { base64 } = await getPlaiceholder(buffer);

return { src, base64 };
}
async function getImageWithPlaceholder(src: string) {
// Extract width and height from Cloudinary URL transformations
const widthMatch = src.match(/w_(\d+)/);
const heightMatch = src.match(/h_(\d+)/);

const originalWidth = widthMatch ? parseInt(widthMatch[1]) : null;
const originalHeight = heightMatch ? parseInt(heightMatch[1]) : null;

// Calculate dimensions for placeholder (max 100px wide, preserve aspect ratio)
const placeholderWidth = 100;
let placeholderHeight: number | undefined;

if (originalWidth && originalHeight) {
// Calculate proportional height based on aspect ratio from URL
const aspectRatio = originalHeight / originalWidth;
placeholderHeight = Math.round(placeholderWidth * aspectRatio);
}

// Build transformation string
const transformations = placeholderHeight
? `w_${placeholderWidth},h_${placeholderHeight},c_fill`
: `w_${placeholderWidth}`;

// Transform Cloudinary URL to fetch a tiny version
// Example transformations:
// https://res.cloudinary.com/dialoghmari/image/upload/w_1200,h_600,c_fill/v1716596284/hello_world_cover_88d282cfe2.jpg
// becomes: https://res.cloudinary.com/dialoghmari/image/upload/w_100,h_50,c_fill/v1716596284/hello_world_cover_88d282cfe2.jpg

const placeholderUrl = src.includes('/upload/')
? src.replace(/\/upload\/(?:.*?\/)?/, `/upload/${transformations}/`)
: src.replace('/upload/', `/upload/${transformations}/`);

const buffer = await fetch(placeholderUrl, {
cache: 'force-cache', // Cache the image fetch
}).then(async (res) =>
Buffer.from(await res.arrayBuffer())
);

const { base64 } = await getPlaiceholder(buffer);

return { src, base64 };
}

What happens:

  • Function parses the Cloudinary URL to extract w_1200 and h_600 transformations
  • Server fetches: A 100px wide thumbnail with preserved aspect ratio (e.g., 100x50 for 1200x600) - only ~2KB
  • User sees: The full-size image with a smooth blur-to-sharp transition

Examples:

  • URL with w_1200,h_600 → fetches 100x50 placeholder (2:1 ratio preserved)
  • URL with w_800,h_600 → fetches 100x75 placeholder (4:3 ratio preserved)
  • URL without transformations → fetches 100px wide, height auto

How it works

  1. Server-side generation: The blur placeholder is generated on the server during the component render
  2. Fetch caching: cache: 'force-cache' ensures the image is fetched once and cached for subsequent builds
  3. Component caching: Next.js caches the result automatically with React Server Components
  4. Better UX: Users see a blurred preview while the full image loads

This approach eliminates layout shift and provides a smooth loading experience without any client-side JavaScript overhead.