211 lines
5.2 KiB
JavaScript
211 lines
5.2 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|
|
})();
|