Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Utiliza técnicas de placeholder para eliminar cambios de diseño y proporcionar retroalimentación visual mientras se cargan las imágenes. Los enfoques principales son codificación de blur hash, esqueletos CSS shimmer, y placeholders de imagen de baja calidad (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 a dimensiones pequeñas para codificación 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);
}Genera 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 imágenes 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")}`;
}Una galería de imágenes con efecto de carga 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 de montaña", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1469474968028-56623f02e42e", alt: "Luz solar en el bosque", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1447752875215-b2761acb3c5d", alt: "Camino forestal", width: 1200, height: 800 },
{ src: "https://images.unsplash.com/photo-1433086966358-54859d0ed716", alt: "Puente de cascada", 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: "Bosque aéreo", 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">Galería con Carga 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 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>
);
}/* Añade a tu CSS global o configuración de tailwind */
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}// tailwind.config.ts - alternativa: define en la configuración de Tailwind
// animation: { shimmer: "shimmer 2s infinite" }
// keyframes: { shimmer: { "100%": { transform: "translateX(100%)" } } }next/image muestra como fondo CSS mientras se carga la imagen completa. Los imports estáticos generan esto automáticamente.next/image maneja la transición de placeholder a imagen cargada automáticamente cuando se usa placeholder="blur". Las transiciones personalizadas requieren el callback onLoad.Renderizado de canvas BlurHash (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} />;
}Import estático con desenfoque automático (enfoque más simple):
import Image from "next/image";
import heroImage from "@/public/images/hero.jpg";
// blurDataURL se genera automáticamente en tiempo de compilación
export function Hero() {
return (
<Image
src={heroImage}
alt="Hero"
placeholder="blur"
priority
className="h-[60vh] w-full object-cover"
/>
);
}Carga progresiva con múltiples tamaños:
"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">
{/* Versión diminuta borrosa - siempre visible 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"
}`}
/>
{/* Imagen de resolución 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 del servidor, necesita datos de píxeles) y decode (lado del cliente, devuelve matriz de píxeles).sharp es una librería solo de Node.js. Úsala solo en Server Components, rutas API, o scripts de compilación.onLoad de next/image está tipado como React.ReactEventHandler<HTMLImageElement>.sharp no puede ejecutarse en el navegador o en Edge Runtime. Úsalo solo en Server Components de Node.js o scripts de tiempo de compilación.blurDataURL muy pequeñas (menos de 4x4 píxeles) pueden producir bandas visibles en lugar de un efecto de desenfoque suave. Utiliza 8x8 o 10x10 para mejores resultados.placeholder="blur" en next/image con imágenes remotas requiere un blurDataURL. Sin él, Next.js lanza un error.blur() en imágenes grandes puede causar problemas de rendimiento en dispositivos de baja potencia. Aplica el desenfoque solo a la imagen placeholder minúscula, nunca a la imagen de resolución completa.onLoad se dispara cuando la imagen se decodifica, que puede ser ligeramente después de que sea visible. Para transiciones pixel-perfectas, considera onLoadingComplete (deprecado en Next.js 14+) o la API nativa decode().| Enfoque | Ventajas | Desventajas |
|---|---|---|
| BlurHash | Carga minúscula (30 caracteres), desenfoque suave | Requiere paso de codificación, decodificación del lado del cliente |
| blurDataURL (next/image) | Incorporado, automático para imports estáticos | Generación manual para imágenes remotas |
| Esqueleto shimmer CSS | Sin datos de imagen necesarios, CSS puro | Genérico, sin vista previa de color |
| LQIP (imagen minúscula) | Vista previa de imagen real, simple | Más grande que cadena BlurHash, solicitud adicional |
| Color dominante | Carga más pequeña (un valor hex) | Sin información espacial, solo color sólido |
| ThumbHash | Mejor calidad que BlurHash, incluye alpha | Más reciente, soporte de ecosistema limitado |
next/image.blurDataURL en tiempo de compilación -- sin trabajo adicional necesario.blurDataURL manualmente.blurDataURL, usar placeholder="blur" en una imagen remota lanza un error.sharp es una librería solo de Node.js.className={`object-cover transition-all duration-700 ${
loadedImages.has(image.src) ? "scale-100 blur-0" : "scale-110 blur-lg"
}`}La imagen comienza escalada y borrosa, luego transiciona a lo normal al cargar a través del 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 del servidor, necesita datos de píxeles) y decode (lado del cliente, devuelve Uint8ClampedArray).sharp es solo de Node.js y devuelve objetos Buffer y metadatos tipados.onLoad de next/image está tipado como React.ReactEventHandler<HTMLImageElement>.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥