The Problem

IntersectionObserver is the standard API for lazy loading images. When it does not fire its callback, images never load, and users see blank spaces where images should be.

Symptoms

  • Images with data-src never load
  • Observer callback never fires
  • Images load on desktop but not mobile
  • Works for some images but not others

Common Causes

Cause 1: Observing Elements Before They Are in the DOM

javascript
// WRONG: Element is not yet in DOM
const img = document.querySelector('.lazy-image');
observer.observe(img); // img is null or not attached

Cause 2: Zero Root Margin

javascript
// Observer only fires when image is VISIBLE
// No preloading, so user sees a blank area first
const observer = new IntersectionObserver(callback, {
  rootMargin: '0px' // No preloading buffer
});

How to Fix It

Fix 1: Correct Observer Setup

```javascript const imageObserver = new IntersectionObserver((entries, observer) => { 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.classList.add('loaded'); observer.unobserve(img); } }); }, { rootMargin: '200px 0px', // Start loading 200px before visible threshold: 0.01 });

// Observe after DOM is ready document.querySelectorAll('.lazy-image').forEach(img => { imageObserver.observe(img); }); ```

Fix 2: Handle Dynamically Added Images

```javascript // For images added after initial page load (infinite scroll) function observeNewImages(container) { container.querySelectorAll('.lazy-image:not([data-observed])').forEach(img => { img.dataset.observed = 'true'; imageObserver.observe(img); }); }

// Call after each dynamic content load observeNewImages(document.querySelector('.gallery')); ```

Fix 3: Check for CSS Overflow Issues

css
/* If any parent has overflow: hidden, it becomes the root */
/* and may prevent intersection detection */
.scroll-container {
  overflow: auto; /* Observer needs this as root */
}
javascript
// Specify the scroll container as root
const observer = new IntersectionObserver(callback, {
  root: document.querySelector('.scroll-container'),
  rootMargin: '200px',
  threshold: 0
});

Fix 4: Fallback for Older Browsers

javascript
if ('IntersectionObserver' in window) {
  // Use IntersectionObserver
} else {
  // Fallback: load all images immediately
  document.querySelectorAll('.lazy-image').forEach(img => {
    img.src = img.dataset.src;
  });
}

Fix 5: Use Native Lazy Loading as Primary

html
<img
  src="placeholder.jpg"
  data-src="real-image.jpg"
  loading="lazy"
  class="fallback-lazy"
>
javascript
// Only use IntersectionObserver if native lazy loading is not supported
if (!('loading' in HTMLImageElement.prototype)) {
  // Use IntersectionObserver polyfill
}