- Standardize script loading on faq, privacy, returns, shipping-info pages - Archive 14 unused JS files (cart-functions, shopping, cart.js, enhanced versions, etc.) - Archive 2 unused CSS files (responsive-enhanced, responsive-fixes) - All pages now use consistent script loading order - Eliminated shopping.js dependency (not needed after standardization)
303 lines
6.8 KiB
JavaScript
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,
|
|
};
|
|
}
|