# Lazy-Load Below-the-Fold Images

**MUST** · **ID:** `performance-lazy-load-below-fold` · **Category:** performance
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/performance-lazy-load-below-fold

> MUST: Eagerly load (and `rel="preload"`) only above-the-fold imagery; give every below-the-fold image `loading="lazy"`. Keep `width`/`height` or `aspect-ratio` on it so deferring the download does not reintroduce layout shift, and never lazy-load the LCP hero.

Give every image below the fold loading="lazy" so it downloads only when approached

Eagerly loading images the user has not scrolled to yet spends their bandwidth and the browser's limited connection pool on pixels nobody is looking at — and it does so while the LCP hero is still competing for those same connections. loading="lazy" defers the request until the image nears the viewport, which cuts initial page weight and lets the above-the-fold content land sooner. Keep width/height (or aspect-ratio) on the placeholder so deferring the download does not reintroduce layout shift, and never lazy-load the hero itself.

## Rule snippet

```tsx
<img src="/chart.png" width="800" height="450" loading="lazy" decoding="async" alt="…">
```

## Bad — do not do this

`performance-lazy-load-below-fold-bad`

```tsx
import { useState } from 'react';

const BASE = 'https://images.pexels.com/photos/1103970/pexels-photo-1103970.jpeg?auto=compress&cs=tinysrgb';
// Distinct widths => distinct resources, so each one is a real separate request.
const WIDTHS = [300, 301, 302, 303, 304, 305, 306, 307];

export function LazyLoadBelowFoldBad() {
  const [loaded, setLoaded] = useState(0);

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="rounded-lg border border-border bg-card p-3">
        <div className="mb-2 flex items-center justify-between text-xs">
          <span className="text-muted-foreground">Scroll the panel ↓</span>
          <span className="font-medium tabular-nums text-error">
            images downloaded: {loaded}/{WIDTHS.length}
          </span>
        </div>

        <div className="h-56 space-y-2 overflow-y-auto rounded-md bg-muted p-2 [overscroll-behavior:contain]">
          {WIDTHS.map((w, i) => (
            <figure key={w} className="space-y-1">
              <img
                src={`${BASE}&w=${w}`}
                alt=""
                width={280}
                height={110}
                loading="eager"
                decoding="async"
                onLoad={() => setLoaded((n) => n + 1)}
                className="h-[110px] w-full rounded object-cover"
              />
              <figcaption className="text-[11px] text-muted-foreground">
                {i === 0 ? 'Hero (above the fold)' : `Card ${i} — below the fold`}
              </figcaption>
            </figure>
          ))}
        </div>
      </div>
      <p className="text-xs text-error">
        Every image is <code>loading=&quot;eager&quot;</code>: the counter hits 8/8 before you scroll
        a single pixel. Seven of those downloads compete with the hero for bandwidth and may never be
        seen.
      </p>
    </div>
  );
}
```

## Good — do this

`performance-lazy-load-below-fold-good`

```tsx
import { useState } from 'react';

const BASE = 'https://images.pexels.com/photos/1103970/pexels-photo-1103970.jpeg?auto=compress&cs=tinysrgb';
// Different widths from the bad example so these are genuinely separate resources.
const WIDTHS = [320, 321, 322, 323, 324, 325, 326, 327];

export function LazyLoadBelowFoldGood() {
  const [loaded, setLoaded] = useState(0);

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="rounded-lg border border-border bg-card p-3">
        <div className="mb-2 flex items-center justify-between text-xs">
          <span className="text-muted-foreground">Scroll the panel ↓</span>
          <span className="font-medium tabular-nums text-success">
            images downloaded: {loaded}/{WIDTHS.length}
          </span>
        </div>

        <div className="h-56 space-y-2 overflow-y-auto rounded-md bg-muted p-2 [overscroll-behavior:contain]">
          {WIDTHS.map((w, i) => {
            const isHero = i === 0;
            return (
              <figure key={w} className="space-y-1">
                <img
                  src={`${BASE}&w=${w}`}
                  alt=""
                  width={280}
                  height={110}
                  // Hero is the LCP candidate: never lazy-load it.
                  loading={isHero ? 'eager' : 'lazy'}
                  fetchPriority={isHero ? 'high' : 'auto'}
                  decoding="async"
                  onLoad={() => setLoaded((n) => n + 1)}
                  className="h-[110px] w-full rounded object-cover"
                />
                <figcaption className="text-[11px] text-muted-foreground">
                  {isHero ? 'Hero — eager + fetchpriority=high' : `Card ${i} — loading="lazy"`}
                </figcaption>
              </figure>
            );
          })}
        </div>
      </div>
      <p className="text-xs text-success">
        The hero loads eagerly; below-the-fold cards are <code>loading=&quot;lazy&quot;</code>, so the
        counter climbs only as you scroll them into view. The hero gets the bandwidth it needs, and
        images you never reach are never downloaded.
      </p>
    </div>
  );
}
```

## References

- [Browser-level lazy loading](https://web.dev/articles/browser-level-image-lazy-loading)
- [img loading attribute (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img)
