Suspense for images allows showing a skeleton/placeholder while the image loads, without useEffect or manual loading state management.
How it works step-by-step:
async component fetches the image and returns a blob URL.<Suspense> with a fallback skeleton.<img> renders.With React 19 (use + fetch):
1import { Suspense, use } from "react";23async function fetchImage(url: string): Promise<string> {4 const res = await fetch(url);5 const blob = await res.blob();6 return URL.createObjectURL(blob);7}89const imageCache = new Map<string, Promise<string>>();1011function getImage(url: string): Promise<string> {12 if (!imageCache.has(url)) {13 imageCache.set(url, fetchImage(url));14 }15 return imageCache.get(url)!;16}1718function AsyncImage({ src, alt }: { src: string; alt: string }) {19 const imageSrc = use(getImage(src));20 return <img src={imageSrc} alt={alt} className="w-full h-auto" loading="lazy" />;21}2223function ImageSkeleton() {24 return (25 <div className="bg-gray-200 animate-pulse rounded w-full h-48" />26 );27}2829function ImageGallery({ images }: { images: { id: string; url: string; alt: string }[] }) {30 return (31 <div className="grid grid-cols-3 gap-4">32 {images.map(img => (33 <Suspense key={img.id} fallback={<ImageSkeleton />}>34 <AsyncImage src={img.url} alt={img.alt} />35 </Suspense>36 ))}37 </div>38 );39}
In Next.js (optimization):
1import Image from "next/image";23// Static import with automatic optimization4import heroImage from "./public/hero.jpg";56<Image7 src={heroImage}8 alt="Hero banner"9 placeholder="blur"10 priority={true} // For LCP images11/>1213// Remote image with blur placeholder14<Image15 src="/photo.jpg"16 alt="Photo"17 width={800}18 height={600}19 placeholder="blur"20 blurDataURL="data:image/jpeg;base64,/9j/..."21 loading="lazy"22/>
Image optimization tips:
loading="lazy" for below-the-fold images.priority={true} for LCP (Largest Contentful Paint) images.srcset for responsive images across screen sizes.width and height to prevent layout shift (CLS).Performance considerations:
imageCache Map prevents re-fetching the same image.Common mistakes:
URL.revokeObjectURL() should be called when the component unmounts to free memory.priority on all images — only the above-the-fold images should have priority={true}.