webupdatev1
This commit is contained in:
302
website/public/assets/js/performance-utils.js
Normal file
302
website/public/assets/js/performance-utils.js
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user