Image Lazy Loading Guide

Method 1: Native loading="lazy" (Recommended)

Browser support: 92%+. No JavaScript needed.

<!-- Always set width and height to prevent layout shift -->
<img
  src="photo.webp"
  loading="lazy"
  width="800"
  height="600"
  alt="Description of image"
>

<!-- Do NOT lazy-load the LCP image (above the fold) -->
<img src="hero.webp" loading="eager" fetchpriority="high" ...>

Method 2: Intersection Observer API

More control — custom thresholds, animations, fallbacks.

// HTML: use data-src instead of src
// <img class="lazy" data-src="photo.webp" alt="...">

const lazyImages = document.querySelectorAll('img.lazy');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      observer.unobserve(img);
    }
  });
}, {
  rootMargin: '200px 0px',  // load 200px before visible
  threshold: 0
});

lazyImages.forEach(img => observer.observe(img));

Blur-Up / LQIP Technique

/* CSS: show a tiny blurred placeholder */
.lazy-wrapper {
  position: relative;
  overflow: hidden;
}
.lazy-placeholder {
  filter: blur(20px);
  transform: scale(1.05); /* hide blur edges */
  transition: opacity 0.4s;
}
.lazy-loaded .lazy-placeholder {
  opacity: 0;
}

Quick Reference

ApproachBrowser SupportJS RequiredCustom Offset
loading="lazy"92%+NoNo
Intersection Observer97%+YesYes
Library (lazysizes)AllYes (5KB)Yes