Performance

Lazy-loading images, measured

With 24 images at 1600 px, eager loading transfers 3.86 MB and loads in 18.9 s; with loading=lazy, 4 KB and 261 ms. The hero stays eager.

Key points

  • Every image lazy: 4,078 bytes at load against 3,858,349 bytes, and load drops from 18,864 to 261 ms.
  • Never make the first screen lazy: in the test the largest image arrived only after scrolling, at 3,552 ms.
  • Eager hero with fetchpriority=high and the rest lazy: 643,104 bytes at load and 3,436 ms.
  • width and height on every img prevent layout shift while the image arrives.

On a gallery of 24 images at 1600×1000 px, lazy loading took the bytes at load from 3,858,349 to 4,078 and the load event from 18,864 to 261 ms. The first screen should never be lazy, though: in the same test the largest image arrived at 3,552 ms, after the scroll.

What loading=lazy changes

VariantBytes at loadTotal requestsLoadImages after scroll
All eager3,858,349 B2518,864 ms24
All lazy4,078 B13261 ms12
Eager hero, lazy rest643,104 B133,436 ms12

The second row is the one that matters: 4 KB against 3.86 MB. The browser fetches only what is about to enter the viewport, and someone reading the first screen does not pay for the other 23 images. The third row shows the cost of the preload margin: with one eager image the browser had already asked for about 640 KB by the load event.

The markup that works



width and height reserve the space and kept CLS at zero in our test. fetchpriority="high" on the first visible element tells the browser not to postpone it.

Why the first screen stays eager

In the all-lazy variant the measured LCP is 3,552 ms against 300 ms in the eager one: the largest image entered the viewport only with the scripted scroll, so the largest paint came late. Lazy loading moves bytes, it does not delete them, and putting it on the hero moves the problem into LCP.

When lazy does not help

  • Images above the fold, including the hero: keep them eager and give them fetchpriority="high". Source: web.dev, CC BY 4.0.
  • Images inside a container that starts hidden: the browser cannot see them and lazy can postpone them too long. Source: MDN, CC BY-SA 2.5.
  • Images that are already small and cached: you save a few KB and still risk a visible gap.

The limitation to name

The test runs on a local server with 150 ms of simulated latency and images that are never cached. On a real connection eager loading does not reach 18.9 s, and over HTTP/2 the 24 requests cost less than they look. The scripted scroll fetched only 12 of 24 images: total bytes depend on how far someone scrolls, which is exactly the point.

Where to continue

The EneUI catalogue indexes 111,985 components, for instance shape-landing-hero, and the block blog-01: a hero with a full-bleed image is the case where lazy must be left out. Read when lazy loading makes things worse, what makes LCP worse and how to set a performance budget.

Frequently asked questions

Does loading=lazy belong on every image?

No. It belongs on everything below the fold; the main image of the first screen stays eager, ideally with fetchpriority=high.

Does lazy loading improve LCP?

Only when the LCP element is not lazy. Applied to the largest image it postpones the paint and took LCP from 300 to 3,552 ms in our test.

Do I also need decoding=async?

It helps keep decoding off the main thread, but it does not replace width and height: without dimensions the layout shifts anyway.

Why does the browser load images I have not seen yet?

Chrome preloads images within a distance from the viewport that depends on the estimated network speed. That is why the hero variant had already fetched 643 KB at load.

Can lazy loading be measured in the field?

Yes, with PerformanceObserver on layout shifts and on Largest Contentful Paint. The lab describes one case; the field shows how far people actually scroll.