Skip to content

Image Loading

Checked by image-dimensions

Images are usually the heaviest thing on a page and the last thing to arrive. How they arrive is part of the design: they can drop into an empty gap and shove the page around, or settle into a space that was waiting for them.

Replay both cards. The network is simulated, so each image takes a moment.

Wrong
Pops in and pushes
Right
Holds its place, fades in

The left image has no size and no placeholder, so there's nothing, then the picture appears all at once and pushes the text down while you're reading it. The right one reserves its box from the first frame, fills it with the photo's average colour, and fades the picture in over 240ms.

Give every image its space and a placeholder before it loads, then fade it in. Except the hero: load that one first and show it at once.

What fills the box

Switch between the placeholder and the loaded image.

Grey box
Average colour
Tiny, unblurred
Tiny, blurred

A grey box is safe but tells you nothing. The average colour hints at the photo for a few bytes. A tiny preview looks like the real image, but only if it's blurred: stretched without blur, it looks like a broken JPEG. BlurHash and ThumbHash encode a smooth preview in about 25 bytes. In dark mode, mix the colour with the page's surface so placeholders don't flash bright.

Don't fade images that are already in the cache, or every revisit flickers. Crossfade with opacity over a still placeholder, and never animate the blur itself.

The hero is different

The main image at the top is usually what Chrome measures as Largest Contentful Paint, which should land within 2.5 seconds. The download is rarely what makes it late. On slow sites, the browser waits about four times longer to start downloading the hero than it spends downloading it.

  • Put the hero in the HTML as a real <img>, not a CSS background or an image added by JavaScript.
  • Mark it fetchpriority="high", and only it.
  • Never lazy-load it. Lazy-load everything below the fold instead.
  • Don't fade it in from invisible. Paints at opacity 0 don't count, so a reveal animation can delay it by seconds.

Usage

app.tsx
<!-- The hero: first in line, shown at once -->
<img src="/hero-1600.avif" width="1600" height="900" alt="…"
     fetchpriority="high">

<!-- Everything else: sized, lazy, sized to its slot -->
<img src="/card-640.avif" width="640" height="400" alt="…"
     loading="lazy" decoding="async"
     srcset="/card-320.avif 320w, /card-640.avif 640w, /card-960.avif 960w"
     sizes="auto, (min-width: 40rem) 45vw, 92vw"
     style="background: var(--placeholder)">
import Image from "next/image";
import hero from "./hero.jpg";

// next/image lazy-loads by default, so the hero must opt out.
<Image src={hero} alt="…" fetchPriority="high" loading="eager"
       sizes="100vw" placeholder="blur" />

// Below the fold: lazy by default, with a blurred placeholder.
<Image src={photo} alt="…" sizes="(min-width: 40rem) 45vw, 92vw"
       placeholder="blur" />

Serve AVIF with WebP and JPEG fallbacks, or let an image CDN pick, and stop at 2× pixel density: the extra pixels on 3× screens are rarely visible. Next.js 16 deprecated priority in favour of preload, but its docs recommend fetchPriority="high" for most heroes. Always set width and height; see Layout Shift.

Resources

Search concepts

Search for a command to run...