Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use técnicas de placeholder para eliminar o deslocamento de layout e fornecer feedback visual enquanto as imagens carregam. As principais abordagens são a codificação blur hash, esqueletos de shimmer CSS e placeholders de imagem de baixa qualidade (LQIP).
Placeholder BlurHash:
npm install blurhash sharp// lib/blurhash.ts
import { encode } from "blurhash";
import sharp from "sharp";
export async function generateBlurHash(imagePath: string): Promise<string> {
const image = sharp(imagePath);
const { width, height } = await image.metadata();
// Redimensiona para dimensões pequenas para codificação rápida
const { data, info } = await image
.resize(32, 32, { fit: "inside" })
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
return encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
}Gerar blurDataURL para next/image:
// lib/blur-data-url.ts
import sharp from "sharp";
export async function generateBlurDataURL(imagePath: string): Promise<string> {
const buffer = await sharp(imagePath)
.resize(8, 8, { fit: "inside" })
.jpeg({ quality: 50 })
.toBuffer();
return `data:image/jpeg;base64,${buffer.toString("base64")}`;
}
// Para imagens remotas
export async function generateRemoteBlurDataURL(imageUrl: string): Promise<string> {
const res = await fetch(imageUrl);
const arrayBuffer = await res.arrayBuffer();
const buffer = await sharp(Buffer.from(arrayBuffer))
.resize(8, 8, { fit: "inside" })
.jpeg({ quality: 50 })
.toBuffer();
return `data:image/jpeg;base64,${buffer.toString("base64")}`;
}Uma grade de imagens com efeito de carregamento blur-up:
// lib/images-with-blur.ts
import { generateRemoteBlurDataURL } from "@/lib/blur-data-url";
interface ImageData {
src: string;
alt: string;
width: number;
height: number;
}
export interface ImageWithBlur extends ImageData {
blurDataURL: string;
}
export async function getImagesWithBlur(images: ImageData[]): Promise<ImageWithBlur[]> {
return Promise.all(
images.map(async (image) => ({
...image,
blurDataURL: await generateRemoteBlurDataURL(image.src),
}))
);
}// app/gallery/page.tsx
import { getImagesWithBlur } from "@/lib/images-with-blur";
import { BlurGallery } from "./blur-gallery";
const imageData = [
{ src: "https://images.unsplash.com/photo-1506744038136-46273834b3fb", alt: "Lago na montanha", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1469474968028-56623f02e42e", alt: "Luz do sol na floresta", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1447752875215-b2761acb3c5d", alt: "Caminho na floresta", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1433086966358-54859d0ed716", alt: "Ponte sobre cachoeira", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1472214103451-9374bd1c798e", alt: "Colinas verdes", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1501854140801-50d01698950b", alt: "Floresta vista de cima", width: 1200, height: 800 },
];
export default async function GalleryPage() {
const images = await getImagesWithBlur(imageData);
return (
<main className="mx-auto max-w-6xl px-4 py-8">
<h1 className="mb-6 text-3xl font-bold">Galeria com Carregamento Blur-Up</h1>
<BlurGallery images={images} />
</main>
);
}// app/gallery/blur-gallery.tsx
"use client";
import Image from "next/image";
import { useState } from "react";
import type { ImageWithBlur } from "@/lib/images-with-blur";
export function BlurGallery({ images }: { images: ImageWithBlur[] }) {
const [loadedImages, setLoadedImages] = useState<Set<string>>(new Set());
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{images.map((image) => (
<div
key={image.src}
className="relative aspect-[3/2] overflow-hidden rounded-xl"
>
<Image
src={image.src}
alt={image.alt}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
placeholder="blur"
blurDataURL={image.blurDataURL}
onLoad={() =>
setLoadedImages((prev) => new Set(prev).add(image.src))
}
className={`object-cover transition-all duration-700 ${
loadedImages.has(image.src)
? "scale-100 blur-0"
: "scale-110 blur-lg"
}`}
/>
</div>
))}
</div>
);
}Placeholder de esqueleto de shimmer CSS:
// components/image-skeleton.tsx
export function ImageSkeleton({ aspectRatio = "3/2" }: { aspectRatio?: string }) {
return (
<div
className="relative overflow-hidden rounded-xl bg-gray-200"
style={{ aspectRatio }}
>
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/60 to-transparent" />
</div>
);
}/* Adicione ao seu CSS global ou configuração do tailwind */
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}// tailwind.config.ts - alternativa: definir na configuração do Tailwind
// animation: { shimmer: "shimmer 2s infinite" }
// keyframes: { shimmer: { "100%": { transform: "translateX(100%)" } } }next/image exibe como um fundo CSS enquanto a imagem completa carrega. Importações estáticas geram isso automaticamente.next/image lida com a transição do placeholder para a imagem carregada automaticamente ao usar placeholder="blur". Transições personalizadas exigem o callback onLoad.Renderização de canvas BlurHash (lado do cliente):
"use client";
import { decode } from "blurhash";
import { useEffect, useRef } from "react";
interface BlurHashCanvasProps {
hash: string;
width: number;
height: number;
className?: string;
}
export function BlurHashCanvas({ hash, width, height, className }: BlurHashCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const pixels = decode(hash, width, height);
const ctx = canvas.getContext("2d");
if (!ctx) return;
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
}, [hash, width, height]);
return <canvas ref={canvasRef} width={width} height={height} className={className} />;
}Importação estática com desfoque automático (abordagem mais simples):
import Image from "next/image";
import heroImage from "@/public/images/hero.jpg";
// blurDataURL é gerado automaticamente no tempo de compilação
export function Hero() {
return (
<Image
src={heroImage}
alt="Hero"
placeholder="blur"
priority
className="h-[60vh] w-full object-cover"
/>
);
}Carregamento progressivo com vários tamanhos:
"use client";
import { useState } from "react";
interface ProgressiveImageProps {
tinyUrl: string;
fullUrl: string;
alt: string;
}
export function ProgressiveImage({ tinyUrl, fullUrl, alt }: ProgressiveImageProps) {
const [loaded, setLoaded] = useState(false);
return (
<div className="relative overflow-hidden">
{/* Versão minúscula desfocada - sempre visível inicialmente */}
<img
src={tinyUrl}
alt=""
aria-hidden
className={`absolute inset-0 h-full w-full scale-110 object-cover blur-xl transition-opacity duration-500 ${
loaded ? "opacity-0" : "opacity-100"
}`}
/>
{/* Imagem de resolução completa */}
<img
src={fullUrl}
alt={alt}
onLoad={() => setLoaded(true)}
className={`relative h-full w-full object-cover transition-opacity duration-500 ${
loaded ? "opacity-100" : "opacity-0"
}`}
/>
</div>
);
}blurhash exporta encode (lado do servidor, precisa de dados de pixel) e decode (lado do cliente, retorna array de pixels).sharp é uma biblioteca exclusiva para Node.js. Use-a apenas em Server Components, rotas de API ou scripts de compilação.next/image onLoad é tipado como React.ReactEventHandler<HTMLImageElement>.sharp não pode ser executado no navegador ou no Edge Runtime. Use-o apenas em Server Components do Node.js ou scripts de tempo de compilação.blurDataURL muito pequenas (abaixo de 4x4 pixels) podem produzir faixas visíveis em vez de um efeito de desfoque suave. Use 8x8 ou 10x10 para melhores resultados.placeholder="blur" em next/image com imagens remotas requer um blurDataURL. Sem ele, Next.js lança um erro.blur() em imagens grandes pode causar problemas de desempenho em dispositivos de baixa potência. Mantenha o desfoque aplicado apenas ao pequeno placeholder, não à imagem de resolução completa.onLoad é disparado quando a imagem é decodificada, o que pode ser ligeiramente após ela se tornar visível. Para transições pixel-perfeitas, considere onLoadingComplete (depreciado no Next.js 14+) ou a API nativa decode().| Abordagem | Prós | Contras |
|---|---|---|
| BlurHash | Carga útil minúscula (30 caracteres), desfoque suave | Requer etapa de codificação, decodificação no lado do cliente |
| blurDataURL (next/image) | Integrado, automático para importações estáticas | Geração manual para imagens remotas |
| Esqueleto de shimmer CSS | Nenhum dado de imagem necessário, CSS puro | Genérico, sem pré-visualização de cor |
| LQIP (imagem minúscula) | Pré-visualização real da imagem, simples | Maior que a string BlurHash, requisição extra |
| Cor Dominante | Carga útil menor (um valor hexadecimal) | Sem informação espacial, apenas cor plana |
| ThumbHash | Melhor qualidade que BlurHash, inclui alfa | Mais novo, menos suporte do ecossistema |
next/image.blurDataURL automaticamente no tempo de compilação -- sem trabalho extra necessário.blurDataURL manualmente.blurDataURL, usar placeholder="blur" em uma imagem remota gera um erro.sharp é uma biblioteca exclusiva para Node.js.className={`object-cover transition-all duration-700 ${
loadedImages.has(image.src) ? "scale-100 blur-0" : "scale-110 blur-lg"
}`}A imagem começa ampliada e desfocada, depois transiciona para o normal ao carregar através do callback onLoad.
<div className="relative overflow-hidden rounded-xl bg-gray-200" style={{ aspectRatio: "3/2" }}>
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite]
bg-gradient-to-r from-transparent via-white/60 to-transparent" />
</div>blurhash exporta encode (lado do servidor, precisa de dados de pixel) e decode (lado do cliente, retorna Uint8ClampedArray).sharp é exclusivo para Node.js e retorna objetos Buffer e metadados tipados.next/image onLoad é tipado como React.ReactEventHandler<HTMLImageElement>.Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥