neumorphic-peripheral
Version:
A lightweight, framework-agnostic JavaScript/TypeScript library for beautiful neumorphic styling
473 lines (472 loc) • 16.2 kB
JavaScript
"use strict";
/**
* Performance optimization utilities for Neumorphic Peripheral
*
* This module provides tools for optimizing component performance,
* monitoring bundle size, and implementing efficient patterns.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.memoryManager = exports.throttleDebounce = exports.lazyLoader = exports.MemoryManager = exports.BundleSizeAnalyzer = exports.VirtualScroller = exports.ThrottleDebounce = exports.LazyLoader = exports.performanceMonitor = void 0;
exports.withPerformanceMonitoring = withPerformanceMonitoring;
exports.autoCleanup = autoCleanup;
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.observers = [];
this.resizeObservers = [];
}
/**
* Measure component initialization time
*/
measureInit(componentName, initFunction) {
const startTime = performance.now();
const result = initFunction();
const endTime = performance.now();
this.updateMetric(componentName, 'componentInitTime', endTime - startTime);
return result;
}
/**
* Measure render time for DOM operations
*/
measureRender(componentName, renderFunction) {
const startTime = performance.now();
renderFunction();
// Use requestAnimationFrame to measure actual render time
requestAnimationFrame(() => {
const endTime = performance.now();
this.updateMetric(componentName, 'renderTime', endTime - startTime);
});
}
/**
* Measure event handler performance
*/
measureEventHandler(componentName, eventHandler) {
return (...args) => {
const startTime = performance.now();
const result = eventHandler(...args);
const endTime = performance.now();
this.updateMetric(componentName, 'eventHandlerTime', endTime - startTime);
return result;
};
}
/**
* Monitor memory usage
*/
measureMemory(componentName) {
if ('memory' in performance) {
const memInfo = performance.memory;
this.updateMetric(componentName, 'memoryUsage', memInfo.usedJSHeapSize);
}
}
/**
* Get performance metrics for a component
*/
getMetrics(componentName) {
return this.metrics.get(componentName);
}
/**
* Get all performance metrics
*/
getAllMetrics() {
const result = {};
this.metrics.forEach((metrics, name) => {
result[name] = metrics;
});
return result;
}
/**
* Reset metrics for a component
*/
resetMetrics(componentName) {
this.metrics.delete(componentName);
}
/**
* Clear all metrics
*/
clearAllMetrics() {
this.metrics.clear();
}
updateMetric(componentName, metricType, value) {
let metrics = this.metrics.get(componentName);
if (!metrics) {
metrics = {
componentInitTime: 0,
renderTime: 0,
eventHandlerTime: 0,
memoryUsage: 0,
bundleSize: 0
};
this.metrics.set(componentName, metrics);
}
metrics[metricType] = value;
}
/**
* Clean up observers
*/
cleanup() {
this.observers.forEach(observer => observer.disconnect());
this.resizeObservers.forEach(observer => observer.disconnect());
this.observers = [];
this.resizeObservers = [];
}
}
// Global performance monitor instance
exports.performanceMonitor = new PerformanceMonitor();
/**
* Lazy loading utilities
*/
class LazyLoader {
constructor() {
this.loadedComponents = new Set();
this.setupIntersectionObserver();
}
/**
* Lazy load a component when it enters viewport
*/
lazyLoad(element, componentLoader, componentName) {
return new Promise((resolve) => {
if (this.loadedComponents.has(componentName)) {
resolve(null);
return;
}
const loadComponent = async () => {
try {
const component = await componentLoader();
this.loadedComponents.add(componentName);
resolve(component);
}
catch (error) {
console.error(`Failed to lazy load component ${componentName}:`, error);
resolve(null);
}
};
// Check if element is already in viewport
const rect = element.getBoundingClientRect();
const isInViewport = rect.top < window.innerHeight && rect.bottom > 0;
if (isInViewport) {
loadComponent();
}
else {
// Wait for element to enter viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadComponent();
observer.unobserve(element);
}
});
});
observer.observe(element);
}
});
}
setupIntersectionObserver() {
if (typeof IntersectionObserver !== 'undefined') {
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.dispatchEvent(new CustomEvent('np:in-viewport'));
}
});
}, { threshold: 0.1 });
}
}
/**
* Clean up resources
*/
cleanup() {
this.intersectionObserver?.disconnect();
this.loadedComponents.clear();
}
}
exports.LazyLoader = LazyLoader;
/**
* Throttling and debouncing utilities
*/
class ThrottleDebounce {
constructor() {
this.throttleTimers = new Map();
this.debounceTimers = new Map();
}
/**
* Throttle function execution
*/
throttle(key, func, limit) {
return (...args) => {
if (!this.throttleTimers.has(key)) {
func.apply(this, args);
this.throttleTimers.set(key, setTimeout(() => {
this.throttleTimers.delete(key);
}, limit));
}
};
}
/**
* Debounce function execution
*/
debounce(key, func, delay) {
return (...args) => {
const existingTimer = this.debounceTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
const timer = setTimeout(() => {
func.apply(this, args);
this.debounceTimers.delete(key);
}, delay);
this.debounceTimers.set(key, timer);
};
}
/**
* Clear all timers
*/
cleanup() {
this.throttleTimers.forEach(timer => clearTimeout(timer));
this.debounceTimers.forEach(timer => clearTimeout(timer));
this.throttleTimers.clear();
this.debounceTimers.clear();
}
}
exports.ThrottleDebounce = ThrottleDebounce;
/**
* Virtual scrolling for large lists
*/
class VirtualScroller {
constructor(container, itemHeight, renderItem) {
this.scrollTop = 0;
this.container = container;
this.itemHeight = itemHeight;
this.renderItem = renderItem;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2;
this.totalItems = 0;
this.setupScrollListener();
}
/**
* Set total number of items
*/
setTotalItems(count) {
this.totalItems = count;
this.updateContainer();
}
/**
* Update visible items based on scroll position
*/
updateVisibleItems() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = Math.min(startIndex + this.visibleItems, this.totalItems);
// Clear container
this.container.innerHTML = '';
// Create spacer for items above viewport
const topSpacer = document.createElement('div');
topSpacer.style.height = `${startIndex * this.itemHeight}px`;
this.container.appendChild(topSpacer);
// Render visible items
for (let i = startIndex; i < endIndex; i++) {
const item = this.renderItem(i);
item.style.height = `${this.itemHeight}px`;
this.container.appendChild(item);
}
// Create spacer for items below viewport
const bottomSpacer = document.createElement('div');
bottomSpacer.style.height = `${(this.totalItems - endIndex) * this.itemHeight}px`;
this.container.appendChild(bottomSpacer);
}
setupScrollListener() {
const throttledUpdate = new ThrottleDebounce().throttle('virtual-scroll', () => this.updateVisibleItems(), 16 // ~60fps
);
this.container.addEventListener('scroll', () => {
this.scrollTop = this.container.scrollTop;
throttledUpdate();
});
}
updateContainer() {
this.container.style.height = `${this.totalItems * this.itemHeight}px`;
this.container.style.overflow = 'auto';
this.updateVisibleItems();
}
/**
* Clean up event listeners
*/
cleanup() {
// Remove scroll listeners (would need to store reference)
}
}
exports.VirtualScroller = VirtualScroller;
/**
* Bundle size analyzer
*/
class BundleSizeAnalyzer {
/**
* Estimate bundle size of imported modules
*/
static async analyzeBundleSize() {
const sizes = {
core: 0,
components: {},
total: 0
};
// Estimate sizes (in production, these would be actual measurements)
sizes.core = this.estimateModuleSize('core', [
'themes', 'utils', 'validators', 'types'
]);
sizes.components.card = this.estimateModuleSize('card', ['base', 'utils']);
sizes.components.input = this.estimateModuleSize('input', ['base', 'utils', 'validators']);
sizes.components.password = this.estimateModuleSize('password', ['input', 'validators']);
sizes.components.button = this.estimateModuleSize('button', ['base', 'utils']);
sizes.components.textarea = this.estimateModuleSize('textarea', ['input', 'utils']);
sizes.components.toggle = this.estimateModuleSize('toggle', ['base', 'utils']);
sizes.total = sizes.core + Object.values(sizes.components).reduce((a, b) => a + b, 0);
return sizes;
}
/**
* Track actual bundle size in production
*/
static trackBundleSize() {
if (typeof window !== 'undefined' && 'performance' in window) {
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
exports.performanceMonitor.updateMetric('bundle', 'bundleSize', navigation.transferSize || 0);
}
}
}
static estimateModuleSize(moduleName, dependencies) {
// Base size estimates (in bytes, minified + gzipped)
const baseSizes = {
core: 3000,
base: 2000,
card: 1500,
input: 2500,
password: 1000, // Additional on top of input
button: 1800,
textarea: 800, // Additional on top of input
toggle: 2200,
themes: 800,
utils: 1200,
validators: 1500,
types: 100 // TypeScript definitions don't add runtime size
};
let size = baseSizes[moduleName] || 1000;
// Add dependency sizes (with some overlap reduction)
dependencies.forEach(dep => {
const depSize = baseSizes[dep] || 500;
size += Math.floor(depSize * 0.3); // Assume 30% of dependency size due to tree-shaking
});
return size;
}
/**
* Recommend optimizations based on usage
*/
static getOptimizationRecommendations(usedComponents) {
const recommendations = [];
const totalComponents = ['card', 'input', 'password', 'button', 'textarea', 'toggle'];
const unusedComponents = totalComponents.filter(c => !usedComponents.includes(c));
if (unusedComponents.length > 0) {
recommendations.push(`Consider tree-shaking unused components: ${unusedComponents.join(', ')}`);
}
if (usedComponents.includes('password') && !usedComponents.includes('input')) {
recommendations.push('Password component includes Input component code - this is expected');
}
if (usedComponents.length === 1) {
recommendations.push('Great! You\'re only importing what you need. Consider named imports for even better tree-shaking.');
}
if (usedComponents.length > 4) {
recommendations.push('Consider using the main bundle import if you\'re using most components');
}
return recommendations;
}
}
exports.BundleSizeAnalyzer = BundleSizeAnalyzer;
/**
* Memory leak detection and prevention
*/
class MemoryManager {
constructor() {
this.componentReferences = new WeakMap();
this.globalListeners = new Map();
}
/**
* Track component event listeners for cleanup
*/
trackEventListener(element, event, listener) {
let listeners = this.componentReferences.get(element);
if (!listeners) {
listeners = new Set();
this.componentReferences.set(element, listeners);
}
listeners.add(listener);
}
/**
* Clean up all listeners for an element
*/
cleanupElement(element) {
const listeners = this.componentReferences.get(element);
if (listeners) {
listeners.clear();
this.componentReferences.delete(element);
}
}
/**
* Detect potential memory leaks
*/
detectMemoryLeaks() {
const recommendations = [];
let potentialLeaks = 0;
// Check for detached DOM nodes with listeners
this.componentReferences.forEach((listeners, element) => {
if (!document.contains(element)) {
potentialLeaks++;
recommendations.push(`Detached element with ${listeners.size} listeners found. Call destroy() method.`);
}
});
// Check global listeners
if (this.globalListeners.size > 10) {
recommendations.push(`${this.globalListeners.size} global listeners registered. Consider cleanup.`);
}
return { potentialLeaks, recommendations };
}
/**
* Force garbage collection (if available)
*/
forceGarbageCollection() {
if ('gc' in window) {
window.gc();
}
}
}
exports.MemoryManager = MemoryManager;
// Global instances
exports.lazyLoader = new LazyLoader();
exports.throttleDebounce = new ThrottleDebounce();
exports.memoryManager = new MemoryManager();
/**
* Performance optimization decorators
*/
function withPerformanceMonitoring(componentName) {
return function (constructor) {
return class extends constructor {
constructor(...args) {
const result = exports.performanceMonitor.measureInit(componentName, () => {
super(...args);
});
// Monitor memory after initialization
setTimeout(() => {
exports.performanceMonitor.measureMemory(componentName);
}, 0);
}
};
};
}
/**
* Cleanup utility for preventing memory leaks
*/
function autoCleanup() {
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
exports.performanceMonitor.cleanup();
exports.lazyLoader.cleanup();
exports.throttleDebounce.cleanup();
});
}
}
// Auto-initialize cleanup
autoCleanup();