Can't get image blur placeholders to work
Answered
Floh posted this in #help-forum
FlohOP
I am trying to setup blur placeholders for my images on my home page in Next 15 but running into some weird issues, and not understanding how this really should work.
When I load my page with cache disabled and 2G throttling I can sometimes see the blurred placeholder images briefly on the page that has no JS or CSS loaded, but then once the JS loads my page appears and there's just empty space where the images should be.
The placeholder never shows after that, and then the images just load from top to bottom in the empty space with a white background, shifting the layout as they appear.
Where could I be going wrong?
This is my page.tsx
This is my
As you can see, I am currently generating the base64 image URL using
When I load my page with cache disabled and 2G throttling I can sometimes see the blurred placeholder images briefly on the page that has no JS or CSS loaded, but then once the JS loads my page appears and there's just empty space where the images should be.
The placeholder never shows after that, and then the images just load from top to bottom in the empty space with a white background, shifting the layout as they appear.
Where could I be going wrong?
This is my page.tsx
// Set revalidation time to 1 hour (3600 seconds)
export const revalidate = 3600
// Force static rendering
export const dynamic = 'force-static'
export default async function HomePage() {
const photos = await getPhotos()
return (
<div>
<Padding className="relative">
<div className="flex flex-col gap-4 mt-8">
{photos.map(
(photo, index) =>
photo.url && <Photo key={photo.id} photo={photo} priority={index < 3} disabled />,
)}
</div>
</Padding>
</div>
)
}
This is my
getPhotos()
server action:export const getPhotos = cache(async (): Promise<Photo[]> => {
const payload = await getPayload({ config })
// Use direct database query instead of API route
const photos = await payload.find({
collection: 'photos',
depth: 1,
overrideAccess: true,
limit: 25,
})
const photosWithBlurDataUrl = await Promise.all(
photos.docs.map(async (photo) => {
if (!photo.url) {
console.error(`[lib/actions/photos] Photo ${photo.id} has no URL`)
return photo
}
const blurDataUrl = await getBase64ImageUrl(photo.url)
return { ...photo, blurDataUrl }
}),
)
// console.log('[lib/actions/photos] fetched photos', photosWithBlurDataUrl)
return photosWithBlurDataUrl
})
As you can see, I am currently generating the base64 image URL using
getBase64ImageUrl()
which I created using sharp functionsimport sharp from 'sharp'
export async function getBase64ImageUrl(imageUrl: string) {
const fullUrl = `${process.env.NEXT_PUBLIC_BASE_URL}${imageUrl}`
const response = await fetch(fullUrl)
const buffer = await response.arrayBuffer()
const resizedImageBuffer = await sharp(buffer).resize(40, null, { fit: 'inside' }).toBuffer()
return `data:image/png;base64,${resizedImageBuffer.toString('base64')}`
}
Answered by Floh
I figured out that something with my custom photo component was cuasing the issue
2 Replies
FlohOP
Also here's the
<Photo>
componentimport { cn } from '@/lib/utils'
import Image from 'next/image'
import Link from 'next/link'
export default function Photo(props: {
photo: Photo
className?: string
disabled?: boolean
priority?: boolean
}) {
const { photo, className, disabled = false, priority = false } = props
if (!photo.url) {
console.error(`[components/photos/photo] Photo ${photo.id} has no URL`)
return null
}
if (!photo.blurDataUrl) {
console.error(`[components/photos/photo] Photo ${photo.id} has no blur data URL`)
return <p>No blur data URL</p>
}
return (
<Link
href={`/photo/${photo.id}`}
className={cn(
'relative w-fit h-full rounded select-none [&_*]:pointer-events-none z-10',
className,
disabled ? 'pointer-events-none' : '',
)}
key={photo.id}
tabIndex={disabled ? -1 : 0}
>
<Image
src={photo.url}
alt={photo.name || ''}
width={2560}
height={1440}
className="w-fit h-full object-cover rounded"
priority={priority}
draggable={false}
placeholder="blur"
blurDataURL={photo.blurDataUrl}
/>
</Link>
)
}
FlohOP
I figured out that something with my custom photo component was cuasing the issue
Answer