UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

390 lines (389 loc) 13.3 kB
/** * Performance Monitoring Utilities for ClarityKit * * Provides comprehensive performance monitoring including Core Web Vitals, * component render times, and bundle size tracking. */ class PerformanceMonitor { constructor() { Object.defineProperty(this, "metrics", { enumerable: true, configurable: true, writable: true, value: {} }); Object.defineProperty(this, "componentMetrics", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "observers", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "isEnabled", { enumerable: true, configurable: true, writable: true, value: false }); this.isEnabled = typeof window !== 'undefined' && 'PerformanceObserver' in window && process.env.NODE_ENV !== 'production'; if (this.isEnabled) { this.initializeObservers(); } } /** * Initialize performance observers for Core Web Vitals */ initializeObservers() { // Largest Contentful Paint try { const lcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; this.metrics.lcp = lastEntry.startTime; this.reportMetric('lcp', lastEntry.startTime); }); lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); this.observers.push(lcpObserver); } catch (error) { console.warn('LCP observer not supported:', error); } // First Input Delay try { const fidObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach((entry) => { const fid = entry.processingStart - entry.startTime; this.metrics.fid = fid; this.reportMetric('fid', fid); }); }); fidObserver.observe({ entryTypes: ['first-input'] }); this.observers.push(fidObserver); } catch (error) { console.warn('FID observer not supported:', error); } // Cumulative Layout Shift try { let cumulativeScore = 0; const clsObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach((entry) => { if (!entry.hadRecentInput) { cumulativeScore += entry.value; } }); this.metrics.cls = cumulativeScore; this.reportMetric('cls', cumulativeScore); }); clsObserver.observe({ entryTypes: ['layout-shift'] }); this.observers.push(clsObserver); } catch (error) { console.warn('CLS observer not supported:', error); } // First Contentful Paint try { const fcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; this.metrics.fcp = lastEntry.startTime; this.reportMetric('fcp', lastEntry.startTime); }); fcpObserver.observe({ entryTypes: ['paint'] }); this.observers.push(fcpObserver); } catch (error) { console.warn('FCP observer not supported:', error); } // Navigation timing for TTFB try { const navigationObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach((entry) => { const ttfb = entry.responseStart - entry.requestStart; this.metrics.ttfb = ttfb; this.reportMetric('ttfb', ttfb); }); }); navigationObserver.observe({ entryTypes: ['navigation'] }); this.observers.push(navigationObserver); } catch (error) { console.warn('Navigation observer not supported:', error); } } /** * Track component performance */ trackComponent(name) { if (!this.isEnabled) { return new ComponentTracker(name, () => { }); } const startTime = performance.now(); const startMemory = this.getMemoryUsage(); return new ComponentTracker(name, (metrics) => { const existing = this.componentMetrics.get(name) || { name, renderTime: 0, mountTime: 0, updateCount: 0 }; const updated = { ...existing, ...metrics, updateCount: existing.updateCount + 1 }; this.componentMetrics.set(name, updated); this.reportComponentMetric(name, updated); }); } /** * Get current memory usage */ getMemoryUsage() { if ('memory' in performance) { return performance.memory.usedJSHeapSize; } return 0; } /** * Report performance metric */ reportMetric(name, value) { if (process.env.NODE_ENV === 'development') { console.log(`[Performance] ${name.toUpperCase()}: ${value.toFixed(2)}ms`); } // Send to analytics service in production if (typeof window !== 'undefined' && window.gtag) { window.gtag('event', 'performance_metric', { metric_name: name, metric_value: Math.round(value), custom_map: { metric_name: 'custom_metric_name' } }); } } /** * Report component performance metric */ reportComponentMetric(name, metrics) { if (process.env.NODE_ENV === 'development') { console.log(`[Component Performance] ${name}:`, { renderTime: `${metrics.renderTime.toFixed(2)}ms`, mountTime: `${metrics.mountTime.toFixed(2)}ms`, updateCount: metrics.updateCount, memoryUsage: metrics.memoryUsage ? `${(metrics.memoryUsage / 1024 / 1024).toFixed(2)}MB` : 'N/A' }); } } /** * Get all collected metrics */ getMetrics() { return { ...this.metrics }; } /** * Get component metrics */ getComponentMetrics() { return Array.from(this.componentMetrics.values()); } /** * Generate performance report */ generateReport() { const metrics = this.getMetrics(); const componentMetrics = this.getComponentMetrics(); let report = '# ClarityKit Performance Report\n\n'; // Core Web Vitals report += '## Core Web Vitals\n\n'; if (metrics.lcp) report += `- **LCP**: ${metrics.lcp.toFixed(2)}ms ${this.getScoreEmoji('lcp', metrics.lcp)}\n`; if (metrics.fid) report += `- **FID**: ${metrics.fid.toFixed(2)}ms ${this.getScoreEmoji('fid', metrics.fid)}\n`; if (metrics.cls) report += `- **CLS**: ${metrics.cls.toFixed(3)} ${this.getScoreEmoji('cls', metrics.cls)}\n`; if (metrics.fcp) report += `- **FCP**: ${metrics.fcp.toFixed(2)}ms ${this.getScoreEmoji('fcp', metrics.fcp)}\n`; if (metrics.ttfb) report += `- **TTFB**: ${metrics.ttfb.toFixed(2)}ms ${this.getScoreEmoji('ttfb', metrics.ttfb)}\n`; // Component Performance if (componentMetrics.length > 0) { report += '\n## Component Performance\n\n'; componentMetrics .sort((a, b) => b.renderTime - a.renderTime) .forEach(metric => { report += `- **${metric.name}**: ${metric.renderTime.toFixed(2)}ms render, ${metric.updateCount} updates\n`; }); } return report; } /** * Get performance score emoji */ getScoreEmoji(metric, value) { const thresholds = { lcp: { good: 2500, poor: 4000 }, fid: { good: 100, poor: 300 }, cls: { good: 0.1, poor: 0.25 }, fcp: { good: 1800, poor: 3000 }, ttfb: { good: 800, poor: 1800 } }; const threshold = thresholds[metric]; if (!threshold) return ''; if (value <= threshold.good) return '✅'; if (value <= threshold.poor) return '⚠️'; return '❌'; } /** * Clean up observers */ destroy() { this.observers.forEach(observer => observer.disconnect()); this.observers = []; this.componentMetrics.clear(); } } /** * Component performance tracker */ class ComponentTracker { constructor(name, onComplete) { Object.defineProperty(this, "startTime", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "startMemory", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "onComplete", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.name = name; this.onComplete = onComplete; this.startTime = performance.now(); this.startMemory = this.getMemoryUsage(); } /** * Mark component as mounted */ markMounted() { const mountTime = performance.now() - this.startTime; const memoryUsage = this.getMemoryUsage() - this.startMemory; this.onComplete({ mountTime, memoryUsage: memoryUsage > 0 ? memoryUsage : undefined }); } /** * Mark render complete */ markRenderComplete() { const renderTime = performance.now() - this.startTime; this.onComplete({ renderTime }); } /** * Get current memory usage */ getMemoryUsage() { if (typeof performance !== 'undefined' && 'memory' in performance) { return performance.memory.usedJSHeapSize; } return 0; } } // Global performance monitor instance export const performanceMonitor = new PerformanceMonitor(); /** * Higher-order component for performance tracking */ export function withPerformanceTracking(Component, componentName) { const name = componentName || Component.name || 'UnknownComponent'; return function TrackedComponent(props) { const tracker = performanceMonitor.trackComponent(name); // Track mount time if (typeof window !== 'undefined') { setTimeout(() => tracker.markMounted(), 0); } // Track render time tracker.markRenderComplete(); return Component(props); }; } /** * Performance measurement decorator for functions */ export function measurePerformance(target, propertyName, descriptor) { const method = descriptor.value; descriptor.value = function (...args) { const startTime = performance.now(); const result = method.apply(this, args); const endTime = performance.now(); if (process.env.NODE_ENV === 'development') { console.log(`[Performance] ${target.constructor.name}.${propertyName}: ${(endTime - startTime).toFixed(2)}ms`); } return result; }; return descriptor; } /** * Bundle size analyzer (for build-time analysis) */ export class BundleSizeAnalyzer { static analyzeBundleSize(bundlePath) { // This would be implemented as a build-time tool // For now, return mock data structure return { totalSize: 0, gzippedSize: 0, chunks: [] }; } static generateSizeReport(metrics) { let report = '# Bundle Size Report\n\n'; report += `**Total Size**: ${(metrics.totalSize / 1024).toFixed(2)} KB\n`; report += `**Gzipped Size**: ${(metrics.gzippedSize / 1024).toFixed(2)} KB\n\n`; if (metrics.chunks.length > 0) { report += '## Chunks\n\n'; metrics.chunks .sort((a, b) => b.size - a.size) .forEach(chunk => { report += `- **${chunk.name}**: ${(chunk.size / 1024).toFixed(2)} KB (${(chunk.gzippedSize / 1024).toFixed(2)} KB gzipped)\n`; }); } return report; } } // Cleanup on page unload if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { performanceMonitor.destroy(); }); }