Lazy-loading delays image downloads until they're near the viewport — saves bandwidth, speeds up initial render, improves LCP. Native browser support has been universal since 2022; you don't need JS libraries any more. The catch: the LCP (hero) image should NOT be lazy-loaded — it should be eager with fetchpriority=high. This guide covers the loading attribute, decoding hints, the LCP exception, and replacing legacy JS lazy-load libraries. For related fixes, see the Image Optimisation Fixes index.
<!-- All images that aren't visible on initial page load -->
<img src="/gallery-photo.jpg"
loading="lazy"
decoding="async"
width="800" height="600"
alt="...">
Browser delays download until the image gets close to the viewport. Bandwidth saved for images the user never scrolls to. Initial render finishes faster.
// React example: Image component defaults to lazy
function ResponsiveImage({ src, alt, width, height, priority = false }) {
return <img
src={src}
alt={alt}
width={width}
height={height}
loading={priority ? 'eager' : 'lazy'}
decoding="async"
fetchpriority={priority ? 'high' : 'auto'}
/>;
}
// Usage
<ResponsiveImage src="/hero.jpg" alt="..." width={1200} height={675} priority /> // LCP
<ResponsiveImage src="/below-fold.jpg" alt="..." width={800} height={600} /> // lazy default
<!-- Hero / LCP image -->
<img src="/hero.jpg"
loading="eager"
fetchpriority="high"
decoding="async"
width="1200" height="675"
alt="...">
loading="eager" — download immediately, don't wait for visibility checkfetchpriority="high" — bump priority over other resourcesdecoding="async" — decode off main thread once downloaded<link rel="preload"
as="image"
href="/hero.jpg"
imagesrcset="/hero-400.jpg 400w, /hero-1200.jpg 1200w, /hero-2400.jpg 2400w"
imagesizes="(max-width: 800px) 100vw, 1200px"
fetchpriority="high">
Goes in <head> before any CSS. Browser starts downloading the hero image as soon as it parses the head, before discovering the img tag in body.
<img src="..." decoding="async" alt="...">
Tells browser to decode the image off the main thread. Once downloaded, decoding doesn't block other JavaScript or paint operations. Universally safe to add to every image (lazy, eager, all of them).
Native lazy-loading has been supported since 2020 in all major browsers. Libraries like lazysizes are typically obsolete.
<!-- OLD: lazysizes pattern --> <img data-src="/photo.jpg" class="lazyload" alt="..."> <script src="lazysizes.min.js"></script> <!-- NEW: native --> <img src="/photo.jpg" loading="lazy" alt="..."> <!-- No JS needed -->
Native loading=lazy applies only to <img> and <iframe> elements. CSS background images don't have a native lazy-load.
<!-- Was background-image, now img -->
<img src="/section-bg.jpg" alt="" loading="lazy" class="full-bleed">
<style>
.full-bleed {
width: 100%;
height: 400px;
object-fit: cover;
}
</style>
<div class="lazy-bg" data-bg="/section-bg.jpg"></div>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.backgroundImage = `url(${entry.target.dataset.bg})`;
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.lazy-bg').forEach(el => observer.observe(el));
</script>
<iframe src="https://www.youtube.com/embed/..."
loading="lazy"
width="560" height="315"
title="...">
</iframe>
Same loading attribute. Browser delays iframe content load until near viewport. Massive savings on pages with embedded YouTube, Vimeo, Twitter widgets, maps.
Verify lazy-load findings are cleared and measure LCP improvement.
Run Image Audit →