// modules/api-manager/PerformanceManager.js import { Logger } from '../../core/logger.js'; /** * Performance APIs Manager - Timing, Metrics, Observers, Optimization */ export class PerformanceManager { constructor(config = {}) { this.config = config; this.marks = new Map(); this.measures = new Map(); this.observers = new Map(); this.metrics = new Map(); this.thresholds = { fcp: 2000, // First Contentful Paint lcp: 2500, // Largest Contentful Paint fid: 100, // First Input Delay cls: 0.1, // Cumulative Layout Shift ...config.thresholds }; // Check API support this.support = { performance: 'performance' in window, timing: 'timing' in (window.performance || {}), navigation: 'navigation' in (window.performance || {}), observer: 'PerformanceObserver' in window, memory: 'memory' in (window.performance || {}), userTiming: 'mark' in (window.performance || {}), resourceTiming: 'getEntriesByType' in (window.performance || {}), paintTiming: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('paint'), layoutInstability: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('layout-shift'), longTask: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('longtask') }; Logger.info('[PerformanceManager] Initialized with support:', this.support); // Auto-start core metrics collection this.startCoreMetrics(); } /** * User Timing API - Marks and Measures */ timing = { // Create performance mark mark: (name, options = {}) => { if (!this.support.userTiming) { Logger.warn('[PerformanceManager] User Timing not supported'); return null; } try { performance.mark(name, options); this.marks.set(name, { name, timestamp: performance.now(), options }); Logger.info(`[PerformanceManager] Mark created: ${name}`); return this.marks.get(name); } catch (error) { Logger.error('[PerformanceManager] Mark creation failed:', error); return null; } }, // Create performance measure measure: (name, startMark, endMark, options = {}) => { if (!this.support.userTiming) { Logger.warn('[PerformanceManager] User Timing not supported'); return null; } try { performance.measure(name, startMark, endMark, options); const entry = performance.getEntriesByName(name, 'measure')[0]; const measure = { name, startTime: entry.startTime, duration: entry.duration, startMark, endMark, options }; this.measures.set(name, measure); Logger.info(`[PerformanceManager] Measure created: ${name} (${measure.duration.toFixed(2)}ms)`); return measure; } catch (error) { Logger.error('[PerformanceManager] Measure creation failed:', error); return null; } }, // Clear marks and measures clear: (name = null) => { if (!this.support.userTiming) return; try { if (name) { performance.clearMarks(name); performance.clearMeasures(name); this.marks.delete(name); this.measures.delete(name); } else { performance.clearMarks(); performance.clearMeasures(); this.marks.clear(); this.measures.clear(); } Logger.info(`[PerformanceManager] Cleared: ${name || 'all'}`); } catch (error) { Logger.error('[PerformanceManager] Clear failed:', error); } }, // Get all marks getMarks: () => { if (!this.support.userTiming) return []; return Array.from(this.marks.values()); }, // Get all measures getMeasures: () => { if (!this.support.userTiming) return []; return Array.from(this.measures.values()); } }; /** * Navigation Timing API */ navigation = { // Get navigation timing data get: () => { if (!this.support.navigation) { return this.getLegacyNavigationTiming(); } try { const entry = performance.getEntriesByType('navigation')[0]; if (!entry) return null; return { // Navigation phases redirect: entry.redirectEnd - entry.redirectStart, dns: entry.domainLookupEnd - entry.domainLookupStart, connect: entry.connectEnd - entry.connectStart, ssl: entry.connectEnd - entry.secureConnectionStart, ttfb: entry.responseStart - entry.requestStart, // Time to First Byte download: entry.responseEnd - entry.responseStart, domProcessing: entry.domContentLoadedEventStart - entry.responseEnd, domComplete: entry.domComplete - entry.domContentLoadedEventStart, loadComplete: entry.loadEventEnd - entry.loadEventStart, // Total times totalTime: entry.loadEventEnd - entry.startTime, // Enhanced metrics navigationStart: entry.startTime, unloadTime: entry.unloadEventEnd - entry.unloadEventStart, redirectCount: entry.redirectCount, transferSize: entry.transferSize, encodedBodySize: entry.encodedBodySize, decodedBodySize: entry.decodedBodySize, // Connection info connectionInfo: { nextHopProtocol: entry.nextHopProtocol, renderBlockingStatus: entry.renderBlockingStatus } }; } catch (error) { Logger.error('[PerformanceManager] Navigation timing failed:', error); return null; } }, // Get performance insights getInsights: () => { const timing = this.navigation.get(); if (!timing) return null; return { insights: { serverResponseTime: this.getInsight('ttfb', timing.ttfb, 200, 500), domProcessing: this.getInsight('domProcessing', timing.domProcessing, 500, 1000), totalLoadTime: this.getInsight('totalTime', timing.totalTime, 2000, 4000), transferEfficiency: this.getTransferEfficiency(timing) }, recommendations: this.getNavigationRecommendations(timing) }; } }; /** * Resource Timing API */ resources = { // Get resource timing data get: (type = null) => { if (!this.support.resourceTiming) { Logger.warn('[PerformanceManager] Resource Timing not supported'); return []; } try { const entries = performance.getEntriesByType('resource'); const resources = entries.map(entry => ({ name: entry.name, type: this.getResourceType(entry), startTime: entry.startTime, duration: entry.duration, size: { transfer: entry.transferSize, encoded: entry.encodedBodySize, decoded: entry.decodedBodySize }, timing: { redirect: entry.redirectEnd - entry.redirectStart, dns: entry.domainLookupEnd - entry.domainLookupStart, connect: entry.connectEnd - entry.connectStart, ssl: entry.connectEnd - entry.secureConnectionStart, ttfb: entry.responseStart - entry.requestStart, download: entry.responseEnd - entry.responseStart }, protocol: entry.nextHopProtocol, cached: entry.transferSize === 0 && entry.decodedBodySize > 0 })); return type ? resources.filter(r => r.type === type) : resources; } catch (error) { Logger.error('[PerformanceManager] Resource timing failed:', error); return []; } }, // Get resource performance summary getSummary: () => { const resources = this.resources.get(); const summary = { total: resources.length, types: {}, totalSize: 0, totalDuration: 0, cached: 0, slowResources: [] }; resources.forEach(resource => { const type = resource.type; if (!summary.types[type]) { summary.types[type] = { count: 0, size: 0, duration: 0 }; } summary.types[type].count++; summary.types[type].size += resource.size.transfer; summary.types[type].duration += resource.duration; summary.totalSize += resource.size.transfer; summary.totalDuration += resource.duration; if (resource.cached) summary.cached++; if (resource.duration > 1000) summary.slowResources.push(resource); }); return summary; } }; /** * Core Web Vitals monitoring */ vitals = { // Start monitoring Core Web Vitals start: (callback = null) => { const vitalsData = {}; // First Contentful Paint this.observePaint((entries) => { entries.forEach(entry => { if (entry.name === 'first-contentful-paint') { vitalsData.fcp = entry.startTime; this.checkThreshold('fcp', entry.startTime); if (callback) callback('fcp', entry.startTime); } }); }); // Largest Contentful Paint this.observeLCP((entries) => { entries.forEach(entry => { vitalsData.lcp = entry.startTime; this.checkThreshold('lcp', entry.startTime); if (callback) callback('lcp', entry.startTime); }); }); // First Input Delay this.observeFID((entries) => { entries.forEach(entry => { vitalsData.fid = entry.processingStart - entry.startTime; this.checkThreshold('fid', vitalsData.fid); if (callback) callback('fid', vitalsData.fid); }); }); // Cumulative Layout Shift this.observeCLS((entries) => { let cls = 0; entries.forEach(entry => { if (!entry.hadRecentInput) { cls += entry.value; } }); vitalsData.cls = cls; this.checkThreshold('cls', cls); if (callback) callback('cls', cls); }); return vitalsData; }, // Get current vitals get: () => { return { fcp: this.getMetric('fcp'), lcp: this.getMetric('lcp'), fid: this.getMetric('fid'), cls: this.getMetric('cls'), ratings: { fcp: this.getRating('fcp', this.getMetric('fcp')), lcp: this.getRating('lcp', this.getMetric('lcp')), fid: this.getRating('fid', this.getMetric('fid')), cls: this.getRating('cls', this.getMetric('cls')) } }; } }; /** * Memory monitoring */ memory = { // Get memory usage get: () => { if (!this.support.memory) { Logger.warn('[PerformanceManager] Memory API not supported'); return null; } try { const memory = performance.memory; return { used: memory.usedJSHeapSize, total: memory.totalJSHeapSize, limit: memory.jsHeapSizeLimit, percentage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100, formatted: { used: this.formatBytes(memory.usedJSHeapSize), total: this.formatBytes(memory.totalJSHeapSize), limit: this.formatBytes(memory.jsHeapSizeLimit) } }; } catch (error) { Logger.error('[PerformanceManager] Memory get failed:', error); return null; } }, // Monitor memory usage monitor: (callback, interval = 5000) => { if (!this.support.memory) return null; const intervalId = setInterval(() => { const memoryData = this.memory.get(); if (memoryData) { callback(memoryData); // Warn if memory usage is high if (memoryData.percentage > 80) { Logger.warn('[PerformanceManager] High memory usage detected:', memoryData.percentage.toFixed(1) + '%'); } } }, interval); return { stop: () => clearInterval(intervalId) }; } }; /** * Long Task monitoring */ longTasks = { // Start monitoring long tasks start: (callback = null) => { if (!this.support.longTask) { Logger.warn('[PerformanceManager] Long Task API not supported'); return null; } try { const observer = new PerformanceObserver((list) => { list.getEntries().forEach(entry => { const taskInfo = { duration: entry.duration, startTime: entry.startTime, name: entry.name, attribution: entry.attribution || [] }; Logger.warn('[PerformanceManager] Long task detected:', taskInfo); if (callback) callback(taskInfo); }); }); observer.observe({ entryTypes: ['longtask'] }); const observerId = this.generateId('longtask'); this.observers.set(observerId, observer); return { id: observerId, stop: () => { observer.disconnect(); this.observers.delete(observerId); } }; } catch (error) { Logger.error('[PerformanceManager] Long task monitoring failed:', error); return null; } } }; /** * Performance optimization utilities */ optimize = { // Defer non-critical JavaScript deferScript: (src, callback = null) => { const script = document.createElement('script'); script.src = src; script.defer = true; if (callback) script.onload = callback; document.head.appendChild(script); return script; }, // Preload critical resources preload: (href, as, crossorigin = false) => { const link = document.createElement('link'); link.rel = 'preload'; link.href = href; link.as = as; if (crossorigin) link.crossOrigin = 'anonymous'; document.head.appendChild(link); return link; }, // Prefetch future resources prefetch: (href) => { const link = document.createElement('link'); link.rel = 'prefetch'; link.href = href; document.head.appendChild(link); return link; }, // Optimize images with lazy loading lazyImages: (selector = 'img[data-src]') => { if ('IntersectionObserver' in window) { const images = document.querySelectorAll(selector); const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img)); return imageObserver; } else { // Fallback for older browsers const images = document.querySelectorAll(selector); images.forEach(img => { img.src = img.dataset.src; img.classList.remove('lazy'); }); } }, // Bundle size analyzer analyzeBundles: () => { const scripts = Array.from(document.querySelectorAll('script[src]')); const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"]')); const analysis = { scripts: scripts.map(script => ({ src: script.src, async: script.async, defer: script.defer })), styles: styles.map(style => ({ href: style.href, media: style.media })), recommendations: [] }; // Add recommendations if (scripts.length > 10) { analysis.recommendations.push('Consider bundling JavaScript files'); } if (styles.length > 5) { analysis.recommendations.push('Consider bundling CSS files'); } return analysis; } }; // Helper methods startCoreMetrics() { // Auto-start vitals monitoring this.vitals.start((metric, value) => { this.setMetric(metric, value); }); // Start memory monitoring if supported if (this.support.memory) { this.memory.monitor((memoryData) => { this.setMetric('memory', memoryData); }); } } observePaint(callback) { if (!this.support.paintTiming) return; try { const observer = new PerformanceObserver((list) => { callback(list.getEntries()); }); observer.observe({ entryTypes: ['paint'] }); return observer; } catch (error) { Logger.error('[PerformanceManager] Paint observer failed:', error); } } observeLCP(callback) { if (!this.support.observer) return; try { const observer = new PerformanceObserver((list) => { callback(list.getEntries()); }); observer.observe({ entryTypes: ['largest-contentful-paint'] }); return observer; } catch (error) { Logger.error('[PerformanceManager] LCP observer failed:', error); } } observeFID(callback) { if (!this.support.observer) return; try { const observer = new PerformanceObserver((list) => { callback(list.getEntries()); }); observer.observe({ entryTypes: ['first-input'] }); return observer; } catch (error) { Logger.error('[PerformanceManager] FID observer failed:', error); } } observeCLS(callback) { if (!this.support.layoutInstability) return; try { const observer = new PerformanceObserver((list) => { callback(list.getEntries()); }); observer.observe({ entryTypes: ['layout-shift'] }); return observer; } catch (error) { Logger.error('[PerformanceManager] CLS observer failed:', error); } } getLegacyNavigationTiming() { if (!this.support.timing) return null; const timing = performance.timing; const navigationStart = timing.navigationStart; return { redirect: timing.redirectEnd - timing.redirectStart, dns: timing.domainLookupEnd - timing.domainLookupStart, connect: timing.connectEnd - timing.connectStart, ssl: timing.connectEnd - timing.secureConnectionStart, ttfb: timing.responseStart - timing.requestStart, download: timing.responseEnd - timing.responseStart, domProcessing: timing.domContentLoadedEventStart - timing.responseEnd, domComplete: timing.domComplete - timing.domContentLoadedEventStart, loadComplete: timing.loadEventEnd - timing.loadEventStart, totalTime: timing.loadEventEnd - navigationStart }; } getResourceType(entry) { const url = new URL(entry.name); const extension = url.pathname.split('.').pop().toLowerCase(); const typeMap = { js: 'script', css: 'stylesheet', png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', svg: 'image', webp: 'image', woff: 'font', woff2: 'font', ttf: 'font', eot: 'font', json: 'fetch', xml: 'fetch' }; return typeMap[extension] || entry.initiatorType || 'other'; } getInsight(metric, value, good, needs) { if (value < good) return { rating: 'good', message: `Excellent ${metric}` }; if (value < needs) return { rating: 'needs-improvement', message: `${metric} needs improvement` }; return { rating: 'poor', message: `Poor ${metric} performance` }; } getTransferEfficiency(timing) { const compressionRatio = timing.decodedBodySize > 0 ? timing.encodedBodySize / timing.decodedBodySize : 1; return { ratio: compressionRatio, rating: compressionRatio < 0.7 ? 'good' : compressionRatio < 0.9 ? 'fair' : 'poor' }; } getNavigationRecommendations(timing) { const recommendations = []; if (timing.ttfb > 500) { recommendations.push('Server response time is slow. Consider optimizing backend performance.'); } if (timing.dns > 100) { recommendations.push('DNS lookup time is high. Consider using a faster DNS provider.'); } if (timing.connect > 1000) { recommendations.push('Connection time is slow. Check network latency.'); } if (timing.domProcessing > 1000) { recommendations.push('DOM processing is slow. Consider optimizing JavaScript execution.'); } return recommendations; } checkThreshold(metric, value) { const threshold = this.thresholds[metric]; if (threshold && value > threshold) { Logger.warn(`[PerformanceManager] ${metric.toUpperCase()} threshold exceeded: ${value}ms (threshold: ${threshold}ms)`); } } getRating(metric, value) { const thresholds = { fcp: { good: 1800, poor: 3000 }, lcp: { good: 2500, poor: 4000 }, fid: { good: 100, poor: 300 }, cls: { good: 0.1, poor: 0.25 } }; const threshold = thresholds[metric]; if (!threshold || value === null || value === undefined) return 'unknown'; if (value <= threshold.good) return 'good'; if (value <= threshold.poor) return 'needs-improvement'; return 'poor'; } setMetric(key, value) { this.metrics.set(key, { value, timestamp: Date.now() }); } getMetric(key) { const metric = this.metrics.get(key); return metric ? metric.value : null; } formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } generateId(prefix = 'perf') { return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Stop all observers and clear data */ cleanup() { this.observers.forEach(observer => { observer.disconnect(); }); this.observers.clear(); this.metrics.clear(); this.marks.clear(); this.measures.clear(); Logger.info('[PerformanceManager] Cleanup completed'); } /** * Get comprehensive performance report */ getReport() { return { support: this.support, navigation: this.navigation.get(), resources: this.resources.getSummary(), vitals: this.vitals.get(), memory: this.memory.get(), marks: this.timing.getMarks(), measures: this.timing.getMeasures(), activeObservers: this.observers.size, timestamp: Date.now() }; } }