Optimizing images is crucial for improving the user experience on web applications. On my personal blog, vmnog.com, I implemented image optimization techniques to ensure a seamless and visually appealing experience for users. In this article, I’ll guide you through the process of optimizing images in Next.js using BlurHash and skeleton loaders.
The Problem
When loading images on a web page, users often experience a flash of blank space before the image loads. This can be jarring and negatively impact the user experience, especially on slower connections. We aim to solve this by providing placeholders that improve the perceived performance of the site.
Initial Setup
First, set up a new Next.js project and install the necessary dependencies.
npx create-next-app@latest my-next-app
cd my-next-app
npm install blurhash
Step 1: Using the Next.js Image Component
Next.js provides an optimized Image component out of the box, which offers better performance compared to a standard <img>
tag.
Example:
import Image from 'next/image';
const BasicImage = () => (
<Image
src="/images/example.jpg"
alt="Example Image"
width={800}
height={600}
/>
);
export default BasicImage;
Using the Next.js Image component provides benefits like automatic resizing, lazy loading, and better performance. However, we can enhance this further by providing visual placeholders.
Step 2: Generating BlurHash Strings
To provide a visual placeholder while the image loads, we use BlurHash. BlurHash generates a blurred, low-resolution placeholder for your images.
You can generate BlurHash strings using the BlurHash website by uploading your images and saving the generated hashes to your database.
Alternatively, you can generate BlurHash strings locally using Node.js. Here’s a script to do that:
blurhash.js:
const { encode } = require('blurhash');
const { createCanvas, loadImage } = require('canvas');
async function getBlurHashFromImage(imagePath) {
const image = await loadImage(imagePath);
const canvas = createCanvas(image.width, image.height);
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
const imageData = context.getImageData(0, 0, image.width, image.height);
return encode(imageData.data, imageData.width, imageData.height, 4, 4);
}
getBlurHashFromImage('path/to/your/image.jpg').then(console.log);
You can run this script using the following command to get the BlurHash in the terminal:
node ./blurhash.js
Save the generated BlurHash string for each image in your database or alongside your image metadata.
Step 3: Creating an Optimized Image Component with BlurHash
Now, let's create a component that uses the BlurHash string as a placeholder.
Add the BlurHash Placeholder:
'use client';
import { Blurhash } from "react-blurhash";
import Image from 'next/image';
const OptimizedImage = ({ src, alt, width, height, blurHash }) => {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div>
{!isLoaded && (
<Blurhash
hash={blurHash}
width={width}
height={height}
resolutionX={32}
resolutionY={32}
punch={1}
/>
)}
<Image
src={src}
alt={alt}
width={width}
height={height}
onLoadingComplete={() => setIsLoaded(true)}
/>
</div>
);
};
export default OptimizedImage;
In this step, we use the BlurHash string as a placeholder while the image is loading. Once the image is loaded, we update the state to show the image and hide the BlurHash.
Step 4: Adding Skeleton Loading
To enhance the user experience further, we can add a skeleton loader that displays until the BlurHash placeholder is ready.
First make sure you have shadcn-ui dependencie installed on your Next.js project, if you don't have it please follow documentation instructions for a detailed guide: shadcn/ui installation guide
Install the shadcn/ui Skeleton Component:
npx shadcn-ui@latest add skeleton
Using the Skeleton Component:
Update the OptimizedImage
component to include the skeleton loader:
'use client';
import { Blurhash } from "react-blurhash";
import Image from 'next/image';
const OptimizedImage = ({ src, alt, width, height, blurHash }) => {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div>
{!isHashLoaded && (
<Skeleton className="absolute inset-0 w-full h-full" />
)}
{!isLoaded && !isHashLoaded && (
<Blurhash
{/* More code... */}
onLoad={() => setIsHashLoaded(true)}
/>
{/* More code... */}
</div>
);
};
export default OptimizedImage;
Step 5: Adding a Delay for Smooth Transition (Optional)
To make the transition from the BlurHash to the actual image smoother, you can add a delay. This ensures that the BlurHash is visible for a short duration, providing a smoother user experience.
Adding a Delay:
import { useEffect } from 'react';
const OptimizedImage = ({ src, alt, width, height, blurHash }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isHashLoaded, setIsHashLoaded] = useState(false);
const [showImage, setShowImage] = useState(false);
// Add a delay to smooth the transition from the BlurHash to the actual image
useEffect(() => {
if (isHashLoaded) {
const timer = setTimeout(() => {
setShowImage(true);
}, 200);
return () => clearTimeout(timer);
}
}, [isHashLoaded]);
// Add a delay to smooth the transition from the Skeleton to the Blurhash
useEffect(() => {
if (isLoaded) {
const timer = setTimeout(() => {
setIsHashLoaded(true);
}, 200);
return () => clearTimeout(timer);
}
}, [isLoaded]);
// More code below...
Final Result: Full Component Code
Add some inline style to make the Image
, Skeleton
and Blurhash
component be located at the same place on the screen. Start copying the styling used below and change it as you need.
Combining all the steps, here is the full OptimizedImage
component:
'use client';
import { Blurhash } from "react-blurhash";
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { Skeleton } from "./ui/skeleton";
const OptimizedImage = ({ src, alt, width, height, blurHash }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isHashLoaded, setIsHashLoaded] = useState(false);
const [showImage, setShowImage] = useState(false);
useEffect(() => {
if (isHashLoaded) {
const timer = setTimeout(() => {
setShowImage(true);
}, 200);
return () => clearTimeout(timer);
}
}, [isHashLoaded]);
useEffect(() => {
if (isLoaded) {
const timer = setTimeout(() => {
setIsHashLoaded(true);
}, 200);
return () => clearTimeout(timer);
}
}, [isLoaded]);
return (
<div className="flex justify-center w-full mt-5">
<div style={{ position: 'relative', width: '100%', maxWidth: width, aspectRatio: `${width} / ${height}` }}>
{!isHashLoaded && (
<Skeleton className="absolute inset-0 w-full h-full" />
)}
{!showImage && blurHash && (
<Blurhash
hash={blurHash}
width="100%"
height="100%"
resolutionX={32}
resolutionY={32}
punch={1}
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', objectFit: 'cover' }}
onLoad={() => setIsHashLoaded(true)}
/>
)}
<Image
src={src}
alt={alt}
layout="fill"
objectFit="cover"
onLoadingComplete={() => setIsLoaded(true)}
style={{
visibility: showImage ? 'visible' : 'hidden',
opacity: showImage ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
}}
/>
</div>
</div>
);
};
export default OptimizedImage;
Utilizing the OptimizedImage
Component in Other Components/Pages
The OptimizedImage
component can be easily integrated into any of your Next.js components or pages to enhance the user experience with optimized image loading. Here are a few examples of how to use the OptimizedImage
component in different contexts within your application.
import OptimizedImage from '@/components/OptimizedImage';
// More code ...
<OptimizedImage
src={post.coverImage}
alt={post.title}
width={800}
height={600}
blurHash={post.coverImageBlurHash}
/>
// More code ...
Conclusion
By following these steps, you have successfully created an optimized image component in Next.js using BlurHash and skeleton loaders. This approach improves the perceived performance and visual appeal of your website. On my blog, vmnog.com, I’ve implemented these techniques to ensure a smooth and engaging user experience.
Implementing these optimizations ensures that your Next.js application handles images efficiently, providing a seamless and visually appealing experience for users.