X Xerobit

Image Lazy Loading — loading='lazy', IntersectionObserver, and LCP

Lazy loading defers off-screen images until they're about to enter the viewport. Learn how HTML loading='lazy' works, when to use eager loading for LCP images,...

Mian Ali Khalid · · 5 min read
Use the tool
Image Compressor
Compress JPEG, PNG, and WebP images in your browser. Adjustable quality, batch mode. Files never leave your device.
Open Image Compressor →

Lazy loading prevents off-screen images from blocking page load. The native loading="lazy" attribute works in all modern browsers. Use it everywhere except for your LCP (Largest Contentful Paint) image.

Use the Image Compressor to reduce image file size before deploying.

Native lazy loading

<!-- Defer loading until image is near viewport: -->
<img src="photo.jpg" loading="lazy" alt="Description" width="800" height="600">

<!-- NEVER use lazy on above-the-fold images: -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero" width="1200" height="600">
<!-- Or simply omit loading= (defaults to eager) -->

Supported in Chrome 77+, Firefox 75+, Safari 15.4+, Edge 79+.

How it works: The browser loads images once they come within ~1200px of the viewport (adjustable by browser heuristic). Exact threshold varies by connection speed.

The LCP image mistake

The most common performance mistake: lazy loading the hero/LCP image:

<!-- BAD: lazy loading the LCP image delays it further -->
<img src="hero.jpg" loading="lazy" alt="Hero">

<!-- GOOD: eager loading + high priority -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero"
     width="1200" height="600">

Always fetchpriority="high" for your LCP image. This tells the browser to prioritize this resource over others.

Preload LCP image in <head>

<head>
  <!-- Preload the LCP image before parser reaches <body>: -->
  <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
  
  <!-- For responsive LCP image: -->
  <link rel="preload" as="image"
        href="/hero-800.jpg"
        imagesrcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
        imagesizes="(max-width: 600px) 100vw, 50vw"
        fetchpriority="high">
</head>

IntersectionObserver for custom lazy loading

Useful when you need more control than loading="lazy":

function lazyLoadImages(selector = 'img[data-src]') {
  const images = document.querySelectorAll(selector);
  
  const observer = new IntersectionObserver((entries, obs) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        
        if (img.dataset.srcset) {
          img.srcset = img.dataset.srcset;
        }
        
        img.removeAttribute('data-src');
        img.removeAttribute('data-srcset');
        obs.unobserve(img);  // Stop observing once loaded
      }
    });
  }, {
    rootMargin: '200px 0px',  // Start loading 200px before entering viewport
    threshold: 0,
  });
  
  images.forEach(img => observer.observe(img));
}

// HTML with data-src instead of src:
// <img data-src="photo.jpg" src="placeholder.jpg" loading="lazy">
lazyLoadImages();

Background images (CSS)

Native loading="lazy" doesn’t work for CSS background-image. Use IntersectionObserver:

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const el = entry.target;
      el.style.backgroundImage = `url(${el.dataset.bg})`;
      el.classList.add('loaded');
      observer.unobserve(el);
    }
  });
}, { rootMargin: '100px' });

document.querySelectorAll('[data-bg]').forEach(el => observer.observe(el));
<div class="hero" data-bg="/hero.jpg" style="background: #eee;">
  <!-- Background image loads lazily -->
</div>

Skeleton loading during lazy load

/* Placeholder while image loads: */
img[loading="lazy"] {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

img.loaded {
  animation: none;
  background: none;
}

Always set width and height

Without width and height, the browser doesn’t know how much space to reserve — causing Cumulative Layout Shift (CLS):

<!-- BAD: No dimensions → layout shift when image loads -->
<img src="photo.jpg" loading="lazy" alt="Photo">

<!-- GOOD: Dimensions prevent CLS -->
<img src="photo.jpg" loading="lazy" alt="Photo" width="800" height="600">

<!-- CSS ensures responsiveness: -->
<style>
img { max-width: 100%; height: auto; }
</style>

Related posts

Related tool

Image Compressor

Compress JPEG, PNG, and WebP images in your browser. Adjustable quality, batch mode. Files never leave your device.

Written by Mian Ali Khalid. Part of the Frontend & Design pillar.