The loading="lazy" attribute tells the browser to defer loading an image or iframe until it's about to enter the viewport — saving bandwidth and speeding up initial load, with no JavaScript.
The loading="lazy" attribute tells the browser to defer loading an image or iframe until it's about to enter the viewport — saving bandwidth and speeding up initial load, with no JavaScript.
lazy — load only when scrolling brings it near the viewport.eager (default) — load immediately, regardless of position.The browser uses a margin (loads slightly before the element scrolls into view) so the image is usually ready by the time the user sees it.
<img src="x.jpg" loading="lazy" width="800" height="600" />
Without dimensions (or an aspect-ratio), the lazily loaded image has no reserved space, so when it loads the page jumps — hurting Cumulative Layout Shift (CLS). Always reserve space.
<!-- ❌ the hero/LCP image should load eagerly — lazy delays it -->
<img src="hero.jpg" loading="lazy" />
<!-- ✅ -->
<img src="hero.jpg" loading="eager" fetchpriority="high" />
Lazy-loading the Largest Contentful Paint image actually hurts performance — it delays the most important pixel. Lazy-load only off-screen media.
// the old manual approach native lazy-loading replaced
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => { if (e.isIntersecting) e.target.src = e.target.dataset.src; });
});
Native loading="lazy" removed the need for this in most cases (though IntersectionObserver is still useful for custom logic).
Native lazy loading cuts initial bytes and requests for long pages with many images — a big mobile win — for free.
The nuances matter: reserve dimensions to protect CLS, and keep above-the-fold/LCP images eager (even fetchpriority="high") so the important content still loads fast.