Files
thrillwiki_django_no_react/static/js/optimization.js
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

337 lines
10 KiB
JavaScript

/**
* 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,
};