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-srcnever 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
// WRONG: Element is not yet in DOM
const img = document.querySelector('.lazy-image');
observer.observe(img); // img is null or not attachedCause 2: Zero Root Margin
// 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
/* If any parent has overflow: hidden, it becomes the root */
/* and may prevent intersection detection */
.scroll-container {
overflow: auto; /* Observer needs this as root */
}// 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
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
<img
src="placeholder.jpg"
data-src="real-image.jpg"
loading="lazy"
class="fallback-lazy"
>// Only use IntersectionObserver if native lazy loading is not supported
if (!('loading' in HTMLImageElement.prototype)) {
// Use IntersectionObserver polyfill
}