/** * Optimized Lazy Loading for Images * Improves page load time by deferring offscreen images * Uses Intersection Observer API for efficient monitoring */ (function () { "use strict"; // Configuration const CONFIG = { rootMargin: "50px", // Start loading 50px before entering viewport threshold: 0.01, loadingClass: "lazy-loading", loadedClass: "lazy-loaded", errorClass: "lazy-error", }; // Image cache to prevent duplicate loads const imageCache = new Set(); /** * Preload image and return promise */ function preloadImage(src) { if (imageCache.has(src)) { return Promise.resolve(); } return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { imageCache.add(src); resolve(); }; img.onerror = reject; img.src = src; }); } /** * Load image with fade-in effect */ async function loadImage(img) { const src = img.dataset.src; const srcset = img.dataset.srcset; if (!src) return; img.classList.add(CONFIG.loadingClass); try { // Preload the image await preloadImage(src); // Set the actual src img.src = src; if (srcset) { img.srcset = srcset; } // Remove data attributes to free memory delete img.dataset.src; delete img.dataset.srcset; // Add loaded class for fade-in animation img.classList.remove(CONFIG.loadingClass); img.classList.add(CONFIG.loadedClass); } catch (error) { console.error("Failed to load image:", src, error); img.classList.remove(CONFIG.loadingClass); img.classList.add(CONFIG.errorClass); // Set fallback/placeholder if available if (img.dataset.fallback) { img.src = img.dataset.fallback; } } } /** * Initialize lazy loading with Intersection Observer */ function initLazyLoad() { // Check for browser support if (!("IntersectionObserver" in window)) { // Fallback: load all images immediately console.warn("IntersectionObserver not supported, loading all images"); const images = document.querySelectorAll("img[data-src]"); images.forEach(loadImage); return; } // Create observer const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target; loadImage(img); observer.unobserve(img); // Stop observing once loaded } }); }, { rootMargin: CONFIG.rootMargin, threshold: CONFIG.threshold, } ); // Observe all lazy images const lazyImages = document.querySelectorAll("img[data-src]"); lazyImages.forEach((img) => observer.observe(img)); // Also observe images added dynamically const mutationObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeName === "IMG" && node.dataset && node.dataset.src) { observer.observe(node); } // Check child images if (node.querySelectorAll) { const childImages = node.querySelectorAll("img[data-src]"); childImages.forEach((img) => observer.observe(img)); } }); }); }); mutationObserver.observe(document.body, { childList: true, subtree: true, }); // Store observers globally for cleanup window._lazyLoadObservers = { observer, mutationObserver }; } /** * Cleanup observers (call on page unload if needed) */ function cleanup() { if (window._lazyLoadObservers) { const { observer, mutationObserver } = window._lazyLoadObservers; observer.disconnect(); mutationObserver.disconnect(); } } // Add CSS for loading states function injectStyles() { if (document.getElementById("lazy-load-styles")) return; const style = document.createElement("style"); style.id = "lazy-load-styles"; style.textContent = ` img[data-src] { opacity: 0; transition: opacity 0.3s ease-in-out; } img.lazy-loading { opacity: 0.5; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: loading 1.5s infinite; } img.lazy-loaded { opacity: 1; } img.lazy-error { opacity: 0.3; border: 2px dashed #ccc; } @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* Prevent layout shift */ img[data-src] { min-height: 200px; background-color: #f5f5f5; } `; document.head.appendChild(style); } // Initialize on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { injectStyles(); initLazyLoad(); }); } else { injectStyles(); initLazyLoad(); } // Export for manual usage window.LazyLoad = { init: initLazyLoad, load: loadImage, cleanup: cleanup, }; })();