/** * ThrillWiki Performance Optimization Module * * Features: * - Advanced lazy loading with Intersection Observer * - Image preloading and progressive loading * - Dynamic module loading (code splitting) * - Memory leak prevention utilities * - Event listener cleanup management */ // ============================================================================= // Event Manager - Memory Leak Prevention // ============================================================================= /** * EventManager class for preventing memory leaks from orphaned event listeners. * Use this for dynamic elements that may be removed from the DOM. */ class EventManager { constructor() { this.listeners = new Map(); } /** * Add an event listener with automatic cleanup tracking * @param {Element} element - The DOM element * @param {string} event - Event type (e.g., 'click') * @param {Function} handler - Event handler function * @param {Object} options - Event listener options */ addEventListener(element, event, handler, options = {}) { if (!element) return; if (!this.listeners.has(element)) { this.listeners.set(element, []); } this.listeners.get(element).push({ event, handler, options }); element.addEventListener(event, handler, options); } /** * Remove all tracked event listeners for an element * @param {Element} element - The DOM element to clean up */ removeListeners(element) { if (!this.listeners.has(element)) return; const handlers = this.listeners.get(element); handlers.forEach(({ event, handler, options }) => { element.removeEventListener(event, handler, options); }); this.listeners.delete(element); } /** * Clean up all tracked event listeners */ cleanup() { this.listeners.forEach((handlers, element) => { handlers.forEach(({ event, handler, options }) => { element.removeEventListener(event, handler, options); }); }); this.listeners.clear(); } } // Global event manager instance window.ThrillWikiEventManager = new EventManager(); // ============================================================================= // Advanced Lazy Loading with Intersection Observer // ============================================================================= /** * LazyLoader class for efficient image lazy loading using Intersection Observer. * Supports progressive loading with blur-up placeholder technique. */ class LazyLoader { constructor(options = {}) { this.options = { rootMargin: options.rootMargin || '50px 0px', 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() { // Check for Intersection Observer support if (!('IntersectionObserver' in window)) { // Fallback: load all images immediately this.loadAllImages(); return; } this.observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { this.loadImage(entry.target); observer.unobserve(entry.target); } }); }, { rootMargin: this.options.rootMargin, threshold: this.options.threshold, }); // Observe all lazy images this.observeImages(); } observeImages() { const lazyImages = document.querySelectorAll('img[data-src], img.lazy'); lazyImages.forEach(img => { if (this.observer) { this.observer.observe(img); } }); } loadImage(img) { const src = img.dataset.src || img.getAttribute('data-src'); const srcset = img.dataset.srcset || img.getAttribute('data-srcset'); if (!src && !srcset) return; img.classList.add(this.options.loadingClass); // Create a temporary image to preload const tempImage = new Image(); tempImage.onload = () => { if (src) img.src = src; if (srcset) img.srcset = srcset; img.classList.remove(this.options.loadingClass); img.classList.add(this.options.loadedClass); img.classList.remove('lazy'); // Trigger custom event for tracking img.dispatchEvent(new CustomEvent('lazyloaded', { bubbles: true })); }; tempImage.onerror = () => { img.classList.remove(this.options.loadingClass); img.classList.add(this.options.errorClass); console.warn('Failed to load image:', src); }; // Start loading tempImage.src = src || ''; if (srcset) tempImage.srcset = srcset; } loadAllImages() { const lazyImages = document.querySelectorAll('img[data-src], img.lazy'); lazyImages.forEach(img => this.loadImage(img)); } // Add new images to observation (for dynamically added content) observe(img) { if (this.observer) { this.observer.observe(img); } else { this.loadImage(img); } } destroy() { if (this.observer) { this.observer.disconnect(); this.observer = null; } } } // ============================================================================= // Dynamic Module Loading (Code Splitting) // ============================================================================= /** * ModuleLoader for lazy loading JavaScript modules only when needed. * Reduces initial bundle size and improves page load time. */ class ModuleLoader { constructor() { this.loadedModules = new Set(); this.loadingPromises = new Map(); } /** * Dynamically load a JavaScript module * @param {string} modulePath - Path to the module * @returns {Promise} - Resolves when module is loaded */ async load(modulePath) { // Return cached if already loaded if (this.loadedModules.has(modulePath)) { return Promise.resolve(); } // Return existing promise if currently loading if (this.loadingPromises.has(modulePath)) { return this.loadingPromises.get(modulePath); } // Create loading promise const loadPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = modulePath; script.async = true; script.onload = () => { this.loadedModules.add(modulePath); this.loadingPromises.delete(modulePath); resolve(); }; script.onerror = () => { this.loadingPromises.delete(modulePath); reject(new Error(`Failed to load module: ${modulePath}`)); }; document.head.appendChild(script); }); this.loadingPromises.set(modulePath, loadPromise); return loadPromise; } /** * Conditionally load a module if an element exists * @param {string} selector - CSS selector to check * @param {string} modulePath - Path to the module to load */ async loadIfNeeded(selector, modulePath) { if (document.querySelector(selector)) { await this.load(modulePath); } } } // Global module loader instance window.ThrillWikiModuleLoader = new ModuleLoader(); // ============================================================================= // Lightbox Functionality (Alpine.js Component) // ============================================================================= document.addEventListener('alpine:init', () => { Alpine.data('lightbox', () => ({ isOpen: false, imgSrc: '', imgAlt: '', open(event) { const img = event.target.tagName === 'IMG' ? event.target : event.target.querySelector('img'); if (img) { // Use full-size image if available via data-full-src this.imgSrc = img.dataset.fullSrc || img.src; this.imgAlt = img.alt; this.isOpen = true; document.body.style.overflow = 'hidden'; } }, close() { this.isOpen = false; this.imgSrc = ''; this.imgAlt = ''; document.body.style.overflow = ''; }, // Cleanup method to prevent memory leaks destroy() { this.close(); } })); }); // ============================================================================= // Initialization // ============================================================================= // Global lazy loader instance let lazyLoader = null; document.addEventListener('DOMContentLoaded', () => { // Initialize lazy loader lazyLoader = new LazyLoader(); window.ThrillWikiLazyLoader = lazyLoader; // Add native lazy loading to images that don't have it const images = document.querySelectorAll('img:not([loading])'); images.forEach(img => { // Set eager for above-the-fold images (first 3 visible images) const rect = img.getBoundingClientRect(); const isAboveFold = rect.top < window.innerHeight && rect.top >= 0; img.setAttribute('loading', isAboveFold ? 'eager' : 'lazy'); }); // Handle HTMX content updates document.body.addEventListener('htmx:afterSwap', (event) => { // Re-observe any new lazy images after HTMX updates if (lazyLoader) { const newImages = event.detail.target.querySelectorAll('img[data-src], img.lazy'); newImages.forEach(img => lazyLoader.observe(img)); } }); }); // Cleanup on page unload window.addEventListener('beforeunload', () => { if (window.ThrillWikiEventManager) { window.ThrillWikiEventManager.cleanup(); } if (lazyLoader) { lazyLoader.destroy(); } }); // ============================================================================= // Export for use in other modules // ============================================================================= window.ThrillWikiOptimization = { EventManager, LazyLoader, ModuleLoader, };