Lazy Loading with Intersection Observer
Efficiently load images only when they enter the viewport for better performance.
Code
/**
* Lazy-load images using IntersectionObserver for better performance.
* Usage: add class="lazy" and data-src="actual.jpg" to <img>; optional data-srcset/data-sizes.
*/
(() => {
'use strict';
// Select all images with the 'lazy' class
const lazyImages = Array.from(document.querySelectorAll('img.lazy'));
// Options for the Intersection Observer
const options = {
root: null, // use viewport as the root
rootMargin: '0px',
threshold: 0.1 // trigger when 10% is in view
};
// Preload a URL to ensure the request succeeds before swapping attributes (avoids flicker/broken UI)
const preload = (url) =>
new Promise((resolve, reject) => {
if (!url) return reject(new Error('Missing data-src for lazy image.'));
const img = new Image();
img.onload = () => resolve();
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
// Function to load image
const loadImage = async (image) => {
try {
if (!(image instanceof HTMLImageElement)) throw new TypeError('Target is not an <img>.');
const { src, srcset, sizes } = image.dataset;
// If already loaded or missing required data, bail early
if (!src || image.src === src) return;
// Preload primary src; browser will handle srcset after swap
await preload(src);
if (srcset) image.srcset = srcset;
if (sizes) image.sizes = sizes;
image.src = src; // Load the image
image.classList.remove('lazy'); // Remove the lazy class
image.removeAttribute('data-src');
image.removeAttribute('data-srcset');
image.removeAttribute('data-sizes');
} catch (error) {
// Fail gracefully: keep placeholder and mark for debugging/monitoring
image.dataset.lazyError = 'true';
console.error('[lazy-load]', error);
}
};
const init = () => {
if (lazyImages.length === 0) return;
// If IntersectionObserver isn't supported, load all immediately (functional fallback)
if (!('IntersectionObserver' in window)) {
lazyImages.forEach((img) => void loadImage(img));
return;
}
// Create the Intersection Observer
const observer = new IntersectionObserver((entries, obs) => {
entries
.filter(({ isIntersecting }) => isIntersecting)
.forEach(({ target }) => {
void loadImage(target);
obs.unobserve(target); // Stop observing
});
}, options);
// Observe each lazy image
lazyImages.forEach((image) => observer.observe(image));
};
// Initialize after DOM is ready (avoids querying before elements exist)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
// Example usage:
// <img class="lazy" src="placeholder.jpg" data-src="image.jpg" data-srcset="image@2x.jpg 2x" alt="..." />
})();