Files
SkyArtShop/website/public/assets/js/performance-utils.js
Local Server c1da8eff42 webupdatev1
2026-01-04 17:52:37 -06:00

303 lines
6.8 KiB
JavaScript

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