1. Install dependencies
npm install thumbhash sharp
2. Copy these files into your project:
"use client";import { DetailedHTMLProps, ImgHTMLAttributes, useState } from "react";import { thumbHashToDataURL } from "thumbhash";import base64ToUint8Array from "@/app/lib/base64ToUint8Array";import { cn } from "@/app/lib/cn";export type ImageProps = DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>,HTMLImageElement>;export default function Image({thumbhash,...props}: ImageProps & { thumbhash: string | null }) {const [loading, setLoading] = useState(true);return (<div className="relative h-full w-full">{loading && thumbhash && (// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text<img{...props}className={cn("absolute inset-0 h-full w-full object-cover transition-opacity duration-300 z-1",props.className)}src={thumbHashToDataURL(base64ToUint8Array(thumbhash))}/>)}{/* eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text */}<img{...props}className={cn("absolute inset-0 h-full w-full object-cover transition-opacity duration-300 z-2 opacity-100",loading && "opacity-0",props.className)}onLoad={(event) => {setLoading(false);props.onLoad?.(event);}}ref={(img) => {if (img?.complete) {setLoading(false);}}}/></div>);}
export default function base64ToUint8Array(base64: string): Uint8Array {const decodedString = atob(base64);const arrayBuffer = new Uint8Array(decodedString.length);for (let i = 0; i < decodedString.length; i++) {arrayBuffer[i] = decodedString.charCodeAt(i);}return arrayBuffer;}
import sharp from 'sharp';import { rgbaToThumbHash } from 'thumbhash';export async function generateThumbHash(arrayBuffer: ArrayBuffer) {const buffer = Buffer.from(arrayBuffer);const sharpImage = sharp(buffer).ensureAlpha();const resizedImage = sharpImage.resize(100, 100, {fit: 'inside',withoutEnlargement: true,});const { data, info } = await resizedImage.raw().toBuffer({ resolveWithObject: true });const rgba = new Uint8Array(data);const thumbHashUint8Array = rgbaToThumbHash(info.width, info.height, rgba);const thumbHashBase64 = Buffer.from(thumbHashUint8Array).toString('base64');return thumbHashBase64;}export default function base64ToUint8Array(base64: string): Uint8Array {const decodedString = atob(base64);const arrayBuffer = new Uint8Array(decodedString.length);for (let i = 0; i < decodedString.length; i++) {arrayBuffer[i] = decodedString.charCodeAt(i);}return arrayBuffer;}
Depending on your needs, you can either:
Here, you'll save the thumbhash in your database alongside the image URL. This is useful when you're already controlling the image URLs in your database.
1. Generate a thumbhash for your image:
import { generateThumbHash } from '@/lib/generateThumbHash';// During image uploadasync function handleImageUpload(file: File) {const arrayBuffer = await file.arrayBuffer();const imageUrl = await uploadImage(file);const thumbhash = await generateThumbHash(arrayBuffer);// Store thumbhash in your databaseawait saveToDatabase({imageUrl,thumbhash});}
2. Use the Image component:
import Image from '@/components/Image';export default async function Page() {const { imageUrl, thumbhash } = await getFromDatabase();return (<Imagesrc={imageUrl}thumbhash={thumbhash}alt="Description"/>);}
import Image from '@/components/Image';export default function Page() {return (<Imagesrc={imageUrl}thumbhash={'YOUR_THUMBHASH'}alt="Description"/>);}
Here, you'll hardcode the thumbhash in your component. This is useful when you're using static images like a landing page.