Skip to content
DocsRenderingImage loading

Image loading

ProgressiveImage wraps next/image with a custom loader and a graceful reveal:

animated skeleton  →  blur placeholder  →  full image
                                        └─ opacity fade, 150ms ease-out

Zero layout shift

Every image's intrinsic dimensions are generated up front, so the layout engine assigns an exact box before anything loads. The parent box is sized; the image fills it. Nothing reflows.

interface ImageData {
  id: string; width: number; height: number;  // known before render → zero CLS
  blurDataURL: string; color: string; url: string; alt: string;
}

The picsum loader

src encodes the seed + intrinsic aspect ratio, so a width-only loader can request any responsive size while preserving the ratio and serving the same photo deterministically:

function picsumLoader({ src, width }) {
  const [seed, w, h] = src.split("|");
  const height = Math.round(width * (Number(h) / Number(w)));
  return `https://picsum.photos/seed/${seed}/${width}/${height}`;
}

Skeleton, priority, and prefetch

  • Skeleton. While unloaded, a transform-only shimmer sweeps across the tile (GPU, disabled under reduced motion), removed the moment the image loads.
  • Priority. First-viewport tiles load eagerly (priority); everything else lazy-loads.
  • Predictive prefetch. During requestIdleCallback, upcoming images are fetched and pre-decoded with img.decode() so they paint without decode jank.
export function preloadAndDecode(image, width) {
  const el = new Image();
  el.src = picsumLoader({ src: picsumSrc(image), width });
  return el.decode().catch(() => undefined);
}