/** * Frontend Performance Optimization Utilities * Provides utilities for optimizing frontend load time and performance */ /** * Lazy load images with IntersectionObserver * More efficient than scroll-based lazy loading */ class OptimizedLazyLoader { constructor(options = {}) { this.options = { rootMargin: options.rootMargin || "50px", threshold: options.threshold || 0.01, loadingClass: options.loadingClass || "lazy-loading", loadedClass: options.loadedClass || "lazy-loaded", errorClass: options.errorClass || "lazy-error", }; this.observer = null; this.init(); } init() { if ("IntersectionObserver" in window) { this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { rootMargin: this.options.rootMargin, threshold: this.options.threshold, } ); this.observeImages(); } else { // Fallback for older browsers this.loadAllImages(); } } handleIntersection(entries) { entries.forEach((entry) => { if (entry.isIntersecting) { this.loadImage(entry.target); this.observer.unobserve(entry.target); } }); } loadImage(img) { const src = img.dataset.src; const srcset = img.dataset.srcset; if (!src) return; img.classList.add(this.options.loadingClass); // Preload the image const tempImg = new Image(); tempImg.onload = () => { img.src = src; if (srcset) img.srcset = srcset; img.classList.remove(this.options.loadingClass); img.classList.add(this.options.loadedClass); img.removeAttribute("data-src"); img.removeAttribute("data-srcset"); }; tempImg.onerror = () => { img.classList.remove(this.options.loadingClass); img.classList.add(this.options.errorClass); console.warn("Failed to load image:", src); }; tempImg.src = src; if (srcset) tempImg.srcset = srcset; } observeImages() { const images = document.querySelectorAll("img[data-src]"); images.forEach((img) => { this.observer.observe(img); }); } loadAllImages() { const images = document.querySelectorAll("img[data-src]"); images.forEach((img) => this.loadImage(img)); } destroy() { if (this.observer) { this.observer.disconnect(); } } } /** * Resource Hints Manager * Adds DNS prefetch, preconnect, and preload hints */ class ResourceHints { static addDNSPrefetch(domains) { domains.forEach((domain) => { if ( !document.querySelector(`link[rel="dns-prefetch"][href="${domain}"]`) ) { const link = document.createElement("link"); link.rel = "dns-prefetch"; link.href = domain; document.head.appendChild(link); } }); } static addPreconnect(urls) { urls.forEach((url) => { if (!document.querySelector(`link[rel="preconnect"][href="${url}"]`)) { const link = document.createElement("link"); link.rel = "preconnect"; link.href = url; link.crossOrigin = "anonymous"; document.head.appendChild(link); } }); } static preloadImage(src, as = "image") { if (!document.querySelector(`link[rel="preload"][href="${src}"]`)) { const link = document.createElement("link"); link.rel = "preload"; link.as = as; link.href = src; document.head.appendChild(link); } } } /** * Debounce with leading edge option * Better for performance-critical events */ function optimizedDebounce(func, wait, options = {}) { let timeout; let lastCallTime = 0; const { leading = false, maxWait = 0 } = options; return function debounced(...args) { const now = Date.now(); const timeSinceLastCall = now - lastCallTime; const callNow = leading && !timeout; const shouldInvokeFromMaxWait = maxWait > 0 && timeSinceLastCall >= maxWait; clearTimeout(timeout); if (callNow) { lastCallTime = now; return func.apply(this, args); } if (shouldInvokeFromMaxWait) { lastCallTime = now; return func.apply(this, args); } timeout = setTimeout(() => { lastCallTime = Date.now(); func.apply(this, args); }, wait); }; } /** * Request Animation Frame Throttle * Ensures maximum one execution per frame */ function rafThrottle(func) { let rafId = null; let lastArgs = null; return function throttled(...args) { lastArgs = args; if (rafId === null) { rafId = requestAnimationFrame(() => { func.apply(this, lastArgs); rafId = null; lastArgs = null; }); } }; } /** * Memory-efficient event delegation */ class EventDelegator { constructor(rootElement = document) { this.root = rootElement; this.handlers = new Map(); } on(eventType, selector, handler) { if (!this.handlers.has(eventType)) { this.handlers.set(eventType, []); this.root.addEventListener( eventType, this.handleEvent.bind(this, eventType) ); } this.handlers.get(eventType).push({ selector, handler }); } handleEvent(eventType, event) { const handlers = this.handlers.get(eventType); if (!handlers) return; const target = event.target; handlers.forEach(({ selector, handler }) => { const element = target.closest(selector); if (element) { handler.call(element, event); } }); } off(eventType, selector) { const handlers = this.handlers.get(eventType); if (!handlers) return; const filtered = handlers.filter((h) => h.selector !== selector); if (filtered.length === 0) { this.root.removeEventListener(eventType, this.handleEvent); this.handlers.delete(eventType); } else { this.handlers.set(eventType, filtered); } } } /** * Batch DOM updates to minimize reflows */ class DOMBatcher { constructor() { this.reads = []; this.writes = []; this.scheduled = false; } read(fn) { this.reads.push(fn); this.schedule(); } write(fn) { this.writes.push(fn); this.schedule(); } schedule() { if (this.scheduled) return; this.scheduled = true; requestAnimationFrame(() => { // Execute all reads first this.reads.forEach((fn) => fn()); this.reads = []; // Then execute all writes this.writes.forEach((fn) => fn()); this.writes = []; this.scheduled = false; }); } } // Export for use if (typeof module !== "undefined" && module.exports) { module.exports = { OptimizedLazyLoader, ResourceHints, optimizedDebounce, rafThrottle, EventDelegator, DOMBatcher, }; } else if (typeof window !== "undefined") { window.PerformanceUtils = { OptimizedLazyLoader, ResourceHints, optimizedDebounce, rafThrottle, EventDelegator, DOMBatcher, }; }