mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 13:31:09 -05:00
- 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.
337 lines
10 KiB
JavaScript
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,
|
|
};
|