diff --git a/client/src/App.tsx b/client/src/App.tsx index 6a4706e..fa829d0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,11 +4,16 @@ import { Toaster } from 'react-hot-toast'; import { ErrorBoundary } from './components/ErrorBoundary'; import { DevPerformanceMonitor } from './components/DevPerformanceMonitor'; import { performanceMonitor } from './utils/performance'; -import { preloadCriticalComponents } from './components/LazyComponents'; +import { + preloadCriticalComponents, + prefetchChunks, + setupViewportPrefetching, + LazyLoginPage, + LazyRegisterPage, + LazyTelegramSettingsPage +} from './components/LazyComponents'; import { useAuthStore } from './store/authStore'; import { useThemeStore } from './store/themeStore'; -import LoginPage from './pages/LoginPage'; -import RegisterPage from './pages/RegisterPage'; import ChatPage from './pages/ChatPage'; import { VerifyEmailPage } from './pages/VerifyEmailPage'; import { socketService } from './services/socket'; @@ -21,9 +26,22 @@ function App() { // Initialize performance monitoring performanceMonitor.trackInteraction('app_init', 'App'); + // Track bundle loading + if ('performance' in window && 'getEntriesByType' in performance) { + const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; + entries.forEach(entry => { + if (entry.name.includes('.js') || entry.name.includes('.css')) { + const bundleName = entry.name.split('/').pop() || 'unknown'; + performanceMonitor.trackBundleLoad(bundleName, entry.transferSize || 0, entry.duration); + } + }); + } + // Preload critical components after initial render const timer = setTimeout(() => { preloadCriticalComponents(); + prefetchChunks(); + setupViewportPrefetching(); }, 1000); loadUser(); @@ -70,7 +88,7 @@ function App() { path="/login" element={ - {isAuthenticated ? : } + {isAuthenticated ? : } } /> @@ -78,7 +96,7 @@ function App() { path="/register" element={ - {isAuthenticated ? : } + {isAuthenticated ? : } } /> @@ -90,6 +108,14 @@ function App() { } /> + + {isAuthenticated ? : } + + } + /> import('../pages/LoginPage'), + { fallback:
Loading Login...
} +); + +export const LazyRegisterPage = createLazyComponent( + () => import('../pages/RegisterPage'), + { fallback:
Loading Register...
} +); +``` + +### 2. Component-Based Code Splitting + +Heavy components are lazy-loaded with proper error boundaries and loading states: + +```typescript +// Heavy UI components +export const LazyUserSettings = createLazyComponent( + () => import('./UserSettings'), + { fallback:
Loading Settings...
} +); + +export const LazyAnalyticsDashboard = createLazyComponent( + () => import('./AnalyticsDashboard'), + { fallback:
Loading Analytics...
} +); +``` + +### 3. Feature-Based Chunk Splitting + +Vite configuration optimizes bundle organization: + +```typescript +// vite.config.ts +manualChunks: { + // Vendor chunks for better caching + 'vendor-react': ['react', 'react-dom'], + 'vendor-router': ['react-router-dom'], + 'vendor-ui': ['lucide-react', 'react-hot-toast'], + + // Feature-based chunks + 'auth-features': ['./src/pages/LoginPage.tsx', './src/pages/RegisterPage.tsx'], + 'chat-features': ['./src/components/ChatWindow.tsx', './src/components/ChatList.tsx'], + 'admin-features': ['./src/components/UserSettings.tsx', './src/components/AnalyticsDashboard.tsx'] +} +``` + +### 4. Intelligent Prefetching Strategies + +#### Hover-Based Prefetching +```typescript +export const preloadOnInteraction = () => { + const settingsButtons = document.querySelectorAll('[data-settings-trigger]'); + settingsButtons.forEach(button => { + button.addEventListener('mouseenter', () => { + import('./UserSettings'); + }, { once: true }); + }); +}; +``` + +#### Viewport Intersection Prefetching +```typescript +export const setupViewportPrefetching = () => { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const componentName = entry.target.dataset.prefetch; + // Load component when it enters viewport + } + }); + }, { rootMargin: '50px' }); +}; +``` + +#### Idle Time Prefetching +```typescript +export const prefetchChunks = () => { + if ('requestIdleCallback' in window) { + requestIdleCallback(() => { + import('../pages/LoginPage'); + import('../pages/RegisterPage'); + }); + } +}; +``` + +## 📊 Performance Monitoring System + +### 1. Core Web Vitals Tracking + +The system automatically monitors all Core Web Vitals: + +- **Largest Contentful Paint (LCP)**: Loading performance +- **First Input Delay (FID)**: Interactivity +- **Cumulative Layout Shift (CLS)**: Visual stability +- **First Contentful Paint (FCP)**: Perceived load speed +- **Time to First Byte (TTFB)**: Server response time + +```typescript +private setupCoreWebVitals() { + // LCP monitoring + const lcpObserver = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1]; + this.recordWebVital('lcp', lastEntry.startTime); + }); + lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); +} +``` + +### 2. Memory Usage Monitoring + +Real-time memory tracking with alerts for high usage: + +```typescript +private setupMemoryMonitoring() { + const recordMemory = () => { + const memory = performance.memory; + const usageRatio = memory.usedJSHeapSize / memory.jsHeapSizeLimit; + + if (usageRatio > 0.8) { + this.triggerPerformanceAlert('memory', usageRatio, 0.8); + } + }; + + setInterval(recordMemory, 10000); +} +``` + +### 3. Bundle Size Analysis + +Automatic tracking of bundle sizes and load times: + +```typescript +trackBundleLoad(bundleName: string, size: number, loadTime: number) { + const metric: BundleMetrics = { + name: bundleName, + size, + loadTime, + timestamp: Date.now(), + }; + + if (size > this.performanceBudgets.bundleSize) { + this.triggerPerformanceAlert('bundleSize', size, this.performanceBudgets.bundleSize); + } +} +``` + +### 4. Component Performance Monitoring + +Track render times and identify slow components: + +```typescript +measureComponentRender(componentName: string) { + const startTime = performance.now(); + + return () => { + const endTime = performance.now(); + const renderTime = endTime - startTime; + + if (renderTime > 16) { // More than one frame at 60fps + console.warn(`Slow component render: ${componentName} took ${renderTime.toFixed(2)}ms`); + } + }; +} +``` + +## 🎯 Performance Budgets + +The system enforces strict performance budgets: + +| Metric | Budget | Alert Threshold | +|---------|--------|-----------------| +| Component Render Time | 16ms | 16ms | +| API Response Time | 1000ms | 1000ms | +| LCP | 2500ms | 2500ms | +| FID | 100ms | 100ms | +| CLS | 0.1 | 0.1 | +| Bundle Size | 250KB | 250KB | +| Memory Usage | 80% | 80% | + +## 📈 Performance Dashboard + +### Real-Time Monitoring Widget + +A floating widget provides real-time performance insights: + +```typescript + +``` + +Features: +- **Performance Score**: Overall performance rating (0-100) +- **Critical Alerts**: Real-time notifications for budget violations +- **Core Web Vitals**: Live metrics display +- **Quick Stats**: Renders, memory, bundle size +- **Recommendations**: Automated optimization suggestions + +### Detailed Analytics Dashboard + +Comprehensive performance analysis with: + +- **Core Web Vitals Visualization**: Color-coded status indicators +- **Memory Usage Tracking**: Heap size and usage percentage +- **Bundle Analysis**: Size distribution and load times +- **Component Performance**: Render times and slow component identification +- **API Performance**: Response times and success rates +- **Performance Alerts**: Historical alert tracking +- **Export Functionality**: Data export for analysis + +## 🔧 Advanced Performance Hook + +### useAdvancedPerformance Hook + +Comprehensive performance monitoring for any component: + +```typescript +const { + performanceData, + isTracking, + trackInteraction, + getRecommendations, + exportData +} = useAdvancedPerformance('MyComponent', { + trackComponentRenders: true, + trackInteractions: true, + trackMemory: true, + enableAlerts: true, + alertThresholds: { + componentRenderTime: 16, + memoryUsage: 0.8, + } +}); +``` + +### Bundle Analyzer Utility + +Automatic bundle analysis with optimization recommendations: + +```typescript +const bundleAnalyzer = new BundleAnalyzer(); + +// Analyze current bundles +const analysis = bundleAnalyzer.analyzeBundles(); + +// Get optimization recommendations +const recommendations = analysis.recommendations; + +// Export analysis data +const data = bundleAnalyzer.exportAnalysis(); +``` + +## 🎨 User Experience Enhancements + +### Smart Loading States + +Contextual loading indicators for different component types: + +```typescript +// Component-specific loading states +{ fallback:
Loading Settings...
} +{ fallback:
Loading Page...
} +``` + +### Error Boundaries + +Graceful error handling for lazy-loaded components: + +```typescript +const LazyComponent = React.lazy(importFunc); + +return ( + }> + }> + + + +); +``` + +### Progressive Loading + +Components load progressively based on usage patterns: + +1. **Critical Components**: Load immediately +2. **Important Components**: Preload on hover +3. **Secondary Components**: Load on idle time +4. **Rare Components**: Load on demand + +## 📊 Performance Metrics + +### Collected Metrics + +1. **Rendering Metrics** + - Component render times + - Total render count + - Slow component identification + +2. **User Interaction Metrics** + - Click response times + - Form submission performance + - Navigation timing + +3. **Network Metrics** + - API response times + - Success rates + - Error tracking + +4. **Resource Metrics** + - Bundle sizes + - Load times + - Cache hit rates + +5. **Core Web Vitals** + - LCP, FID, CLS, FCP, TTFB + - Historical trends + - Budget compliance + +### Alert System + +Real-time alerts for performance violations: + +```typescript +// Performance alert structure +interface PerformanceAlert { + metric: string; + value: number; + budget: number; + timestamp: number; + url: string; + severity: 'low' | 'medium' | 'high'; +} +``` + +## 🚀 Optimization Recommendations + +The system automatically generates optimization recommendations: + +### Code Splitting Recommendations +- "Consider code splitting for 3 large bundle(s) over 250KB" +- "Consider lazy loading 2 feature-specific bundle(s)" + +### Performance Recommendations +- "Optimize 5 slow component(s) with render times > 16ms" +- "Improve Largest Contentful Paint by optimizing loading performance" + +### Memory Recommendations +- "High memory usage detected. Consider memory optimization techniques" + +### Bundle Recommendations +- "Found duplicate dependencies: react, react-dom. Consider vendor chunk optimization" +- "Multiple vendor bundles detected. Consider consolidating into a single vendor chunk" + +## 📱 Implementation Benefits + +### For Users +- **Faster Initial Load**: Reduced initial bundle size +- **Improved Interactivity**: Better FID scores +- **Smoother Experience**: Reduced layout shifts +- **Progressive Loading**: Content loads as needed + +### For Developers +- **Real-Time Monitoring**: Immediate performance feedback +- **Automated Alerts**: Proactive issue detection +- **Detailed Analytics**: Comprehensive performance data +- **Optimization Guidance**: Actionable recommendations + +### For the Business +- **Better User Experience**: Higher engagement and retention +- **Improved SEO**: Better Core Web Vitals scores +- **Reduced Costs**: Optimized resource usage +- **Scalability**: Performance-aware architecture + +## 🔮 Future Enhancements + +### Planned Features +1. **Service Worker Integration**: Advanced caching strategies +2. **Predictive Prefetching**: AI-based content prediction +3. **Performance A/B Testing**: Compare optimization strategies +4. **Real User Monitoring (RUM)**: Collect real-world performance data +5. **Performance Budget Enforcement**: Automated build failures for budget violations + +### Monitoring Expansion +1. **Network Performance**: Connection quality monitoring +2. **Device Performance**: Hardware capability detection +3. **Geographic Performance**: Regional performance analysis +4. **User Journey Performance**: End-to-end experience tracking + +## 📚 Usage Guidelines + +### Adding New Components +1. Use lazy loading for components > 50KB +2. Implement proper loading states +3. Add performance monitoring hooks +4. Set appropriate prefetching strategies + +### Performance Budget Management +1. Monitor Core Web Vitals regularly +2. Keep bundle sizes under limits +3. Optimize images and assets +4. Implement efficient caching + +### Monitoring Best Practices +1. Use performance monitoring hooks +2. Set up alerts for critical metrics +3. Export and analyze performance data +4. Continuously optimize based on recommendations + +This comprehensive performance implementation ensures optimal user experience while providing developers with the tools needed to maintain and improve application performance over time. \ No newline at end of file diff --git a/client/src/components/ChatList.tsx b/client/src/components/ChatList.tsx index 4d90c8c..2f7f57d 100644 --- a/client/src/components/ChatList.tsx +++ b/client/src/components/ChatList.tsx @@ -5,7 +5,7 @@ import { useAuthStore } from '../store/authStore'; import { getChatName, getChatAvatar, formatMessageTime, getInitials } from '../utils/helpers'; import NewChatModal from './NewChatModal'; import ThemeToggle from './ThemeToggle'; -import { UserSettings } from './UserSettings'; +import UserSettings from './UserSettings'; interface ChatListProps { onSelectChat: (chatId: string) => void; diff --git a/client/src/components/LazyComponents.tsx b/client/src/components/LazyComponents.tsx index 0030ec6..31b7b25 100644 --- a/client/src/components/LazyComponents.tsx +++ b/client/src/components/LazyComponents.tsx @@ -66,6 +66,42 @@ export const LazyVirtualizedList = createLazyComponent( { fallback:
Loading List...
} ); +export const LazyPerformanceDashboard = createLazyComponent( + () => import('./PerformanceDashboard').then(module => ({ default: module.PerformanceDashboard })), + { fallback:
Loading Performance Dashboard...
} +); + +export const LazyNewChatModal = createLazyComponent( + () => import('./NewChatModal'), + { fallback:
Loading New Chat...
} +); + +export const LazyEmojiPicker = createLazyComponent( + () => import('emoji-picker-react'), + { fallback:
Loading Emoji Picker...
} +); + +// Route-based lazy components +export const LazyLoginPage = createLazyComponent( + () => import('../pages/LoginPage'), + { fallback:
Loading Login...
} +); + +export const LazyRegisterPage = createLazyComponent( + () => import('../pages/RegisterPage'), + { fallback:
Loading Register...
} +); + +export const LazyTelegramSettingsPage = createLazyComponent( + () => import('../pages/TelegramSettingsPage'), + { fallback:
Loading Telegram Settings...
} +); + +export const LazyTelegramMiniApp = createLazyComponent( + () => import('../pages/TelegramMiniApp'), + { fallback:
Loading Mini App...
} +); + // Preload functions for critical components export const preloadCriticalComponents = () => { // Preload components that are likely to be used soon @@ -91,4 +127,64 @@ export const preloadOnInteraction = () => { import('./AnalyticsDashboard'); }, { once: true }); }); + + // Preload bot manager when hovering over bot-related elements + const botTriggers = document.querySelectorAll('[data-bot-trigger]'); + botTriggers.forEach(trigger => { + trigger.addEventListener('mouseenter', () => { + import('./BotManager'); + }, { once: true }); + }); +}; + +// Prefetch chunks for better performance +export const prefetchChunks = () => { + // Prefetch auth-related chunks + if ('requestIdleCallback' in window) { + requestIdleCallback(() => { + import('../pages/LoginPage'); + import('../pages/RegisterPage'); + import('../pages/VerifyEmailPage'); + }); + } +}; + +// Intersection Observer for prefetching components when they come into viewport +export const setupViewportPrefetching = () => { + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const element = entry.target as HTMLElement; + const componentName = element.dataset.prefetch; + + if (componentName) { + switch (componentName) { + case 'analytics': + import('./AnalyticsDashboard'); + break; + case 'settings': + import('./UserSettings'); + break; + case 'bots': + import('./BotManager'); + break; + case 'media': + import('./MediaViewer'); + break; + case 'voice': + import('./VoiceRecorder'); + break; + } + observer.unobserve(element); + } + } + }); + }, { rootMargin: '50px' }); + + // Observe elements with prefetch data attributes + document.querySelectorAll('[data-prefetch]').forEach(element => { + observer.observe(element); + }); + } }; \ No newline at end of file diff --git a/client/src/components/PerformanceDashboard.tsx b/client/src/components/PerformanceDashboard.tsx index 778e604..f5ee14f 100644 --- a/client/src/components/PerformanceDashboard.tsx +++ b/client/src/components/PerformanceDashboard.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Activity, Clock, Zap, AlertTriangle, TrendingUp, BarChart3 } from 'lucide-react'; +import { Activity, Clock, Zap, AlertTriangle, TrendingUp, BarChart3, Cpu, Package, HardDrive } from 'lucide-react'; import { performanceMonitor } from '../utils/performance'; interface PerformanceSummary { @@ -11,11 +11,30 @@ interface PerformanceSummary { slowApiCallsCount: number; totalInteractions: number; apiSuccessRate: number; + coreWebVitals: { + lcp: number; + fid: number; + cls: number; + fcp: number; + ttfb: number; + }; + bundles: { + totalSize: number; + averageLoadTime: number; + count: number; + }; + memory: { + usageRatio: number; + usedHeapMB: number; + totalHeapMB: number; + }; + performanceAlertsCount: number; } export const PerformanceDashboard: React.FC = () => { const [summary, setSummary] = useState(null); const [showDetails, setShowDetails] = useState(false); + const [alerts, setAlerts] = useState([]); useEffect(() => { const updateSummary = () => { @@ -23,19 +42,28 @@ export const PerformanceDashboard: React.FC = () => { setSummary(currentSummary); }; + const handlePerformanceAlert = (event: CustomEvent) => { + setAlerts(prev => [event.detail, ...prev.slice(0, 9)]); // Keep last 10 alerts + }; + updateSummary(); // Auto-refresh every 5 seconds const interval = setInterval(updateSummary, 5000); + // Listen for performance alerts + window.addEventListener('performanceAlert', handlePerformanceAlert as EventListener); + return () => { if (interval) clearInterval(interval); + window.removeEventListener('performanceAlert', handlePerformanceAlert as EventListener); }; }, []); const handleClearMetrics = () => { performanceMonitor.clearMetrics(); setSummary(performanceMonitor.getPerformanceSummary()); + setAlerts([]); }; const handleExportMetrics = () => { @@ -51,184 +79,260 @@ export const PerformanceDashboard: React.FC = () => { URL.revokeObjectURL(url); }; + const getVitalStatus = (value: number, threshold: number, inverse = false) => { + const isGood = inverse ? value <= threshold : value <= threshold; + return isGood ? 'text-green-600' : value <= threshold * 1.5 ? 'text-yellow-600' : 'text-red-600'; + }; + + const formatBytes = (bytes: number) => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }; + if (!summary) { return (
- +
); } - const getPerformanceColor = (value: number, threshold: number, inverse = false) => { - if (inverse) { - return value >= threshold ? 'text-green-600' : value >= threshold * 0.7 ? 'text-yellow-600' : 'text-red-600'; - } - return value <= threshold ? 'text-green-600' : value <= threshold * 1.5 ? 'text-yellow-600' : 'text-red-600'; - }; - return (
-
-
- -

- Performance Monitor -

-
- +
+

+ + Performance Dashboard +

- -
- {/* Performance Metrics Grid */} -
- {/* Component Performance */} -
-
- -

Components

-
-
-
- {summary.totalComponentRenders} -
-
- Avg: - {summary.averageComponentRenderTime.toFixed(2)}ms - -
- {summary.slowComponentsCount > 0 && ( -
- - {summary.slowComponentsCount} slow renders + {/* Performance Alerts */} + {alerts.length > 0 && ( +
+

+ + Performance Alerts ({alerts.length}) +

+
+ {alerts.map((alert, index) => ( +
+ {alert.metric}: {alert.value.toFixed(2)} (budget: {alert.budget}) + + {new Date(alert.timestamp).toLocaleTimeString()} +
- )} + ))}
+ )} - {/* API Performance */} -
-
- -

API Calls

-
-
-
- {summary.totalApiCalls} -
-
- Avg: - {summary.averageApiCallTime.toFixed(2)}ms - -
- {summary.slowApiCallsCount > 0 && ( -
- - {summary.slowApiCallsCount} slow calls -
- )} + {/* Core Web Vitals */} +
+
+
+ LCP + +
+
+ {summary.coreWebVitals.lcp.toFixed(0)}ms
+
Largest Contentful Paint
- {/* Success Rate */} -
-
- -

Success Rate

+
+
+ FID +
-
-
- {summary.apiSuccessRate.toFixed(1)}% -
-
- API reliability -
-
- {summary.apiSuccessRate >= 95 ? 'Excellent' : - summary.apiSuccessRate >= 90 ? 'Good' : 'Needs Improvement'} -
+
+ {summary.coreWebVitals.fid.toFixed(0)}ms
+
First Input Delay
- {/* User Interactions */} -
-
- -

Interactions

+
+
+ CLS +
-
-
- {summary.totalInteractions} -
-
- User actions tracked -
-
- Active monitoring -
+
+ {summary.coreWebVitals.cls.toFixed(3)} +
+
Cumulative Layout Shift
+
+ +
+
+ FCP + +
+
+ {summary.coreWebVitals.fcp.toFixed(0)}ms +
+
First Contentful Paint
+
+ +
+
+ TTFB + +
+
+ {summary.coreWebVitals.ttfb.toFixed(0)}ms
+
Time to First Byte
- {/* Performance Tips */} -
-

Performance Tips

-
    - {summary.slowComponentsCount > 0 && ( -
  • • Consider optimizing components with render times > 16ms
  • - )} - {summary.slowApiCallsCount > 0 && ( -
  • • Some API calls are taking longer than 1 second
  • - )} - {summary.apiSuccessRate < 95 && ( -
  • • API success rate could be improved for better reliability
  • - )} - {summary.slowComponentsCount === 0 && summary.slowApiCallsCount === 0 && summary.apiSuccessRate >= 95 && ( -
  • • Great job! All performance metrics are within acceptable ranges
  • - )} -
+
+ {/* Memory Usage */} +
+
+ Memory + +
+
+ {(summary.memory.usageRatio * 100).toFixed(1)}% +
+
+ {summary.memory.usedHeapMB.toFixed(1)}MB / {summary.memory.totalHeapMB.toFixed(1)}MB +
+
+ + {/* Bundle Size */} +
+
+ Bundles + +
+
+ {formatBytes(summary.bundles.totalSize)} +
+
+ {summary.bundles.count} bundles • {summary.bundles.averageLoadTime.toFixed(0)}ms avg load +
+
+ + {/* Component Performance */} +
+
+ Components + +
+
+ {summary.averageComponentRenderTime.toFixed(1)}ms +
+
+ {summary.totalComponentRenders} renders • {summary.slowComponentsCount} slow +
+
+ + {/* API Performance */} +
+
+ API + +
+
+ {summary.averageApiCallTime.toFixed(0)}ms +
+
+ {summary.apiSuccessRate.toFixed(1)}% success • {summary.slowApiCallsCount} slow +
+
- {/* Detailed Metrics */} {showDetails && ( -
-

Detailed Metrics

+
+

Detailed Metrics

-

Component Metrics

-
-
Total Renders: {summary.totalComponentRenders}
-
Average Time: {summary.averageComponentRenderTime.toFixed(2)}ms
-
Slow Renders: {summary.slowComponentsCount}
+

Rendering

+
+
+ Total Renders: + {summary.totalComponentRenders} +
+
+ Avg Render Time: + {summary.averageComponentRenderTime.toFixed(2)}ms +
+
+ Slow Components: + {summary.slowComponentsCount} +
- + +
+

API Calls

+
+
+ Total Calls: + {summary.totalApiCalls} +
+
+ Avg Response Time: + {summary.averageApiCallTime.toFixed(2)}ms +
+
+ Success Rate: + {summary.apiSuccessRate.toFixed(1)}% +
+
+
+ +
+

User Interactions

+
+
+ Total Interactions: + {summary.totalInteractions} +
+
+ Performance Alerts: + {summary.performanceAlertsCount} +
+
+
+
-

API Metrics

-
-
Total Calls: {summary.totalApiCalls}
-
Average Time: {summary.averageApiCallTime.toFixed(2)}ms
-
Slow Calls: {summary.slowApiCallsCount}
+

Bundle Analysis

+
+
+ Total Size: + {formatBytes(summary.bundles.totalSize)} +
+
+ Bundle Count: + {summary.bundles.count} +
+
+ Avg Load Time: + {summary.bundles.averageLoadTime.toFixed(2)}ms +
diff --git a/client/src/components/PerformanceWidget.tsx b/client/src/components/PerformanceWidget.tsx new file mode 100644 index 0000000..9dc91e6 --- /dev/null +++ b/client/src/components/PerformanceWidget.tsx @@ -0,0 +1,292 @@ +import React, { useState, useEffect } from 'react'; +import { + Activity, + AlertTriangle, + Monitor, + Download, + RefreshCw, + Eye, + EyeOff, + Settings +} from 'lucide-react'; +import { useAdvancedPerformance } from '../hooks/useAdvancedPerformance'; + +interface PerformanceWidgetProps { + componentName?: string; + minimal?: boolean; + showOnlyAlerts?: boolean; + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; +} + +export const PerformanceWidget: React.FC = ({ + componentName = 'App', + minimal = false, + showOnlyAlerts = false, + position = 'top-right' +}) => { + const [isVisible, setIsVisible] = useState(true); + const [isExpanded, setIsExpanded] = useState(false); + const [realTimeMode, setRealTimeMode] = useState(true); + + const { + performanceData, + startTracking, + stopTracking, + getRecommendations, + exportData, + clearData + } = useAdvancedPerformance(componentName, { + trackComponentRenders: true, + trackInteractions: true, + enableAlerts: true, + }); + + useEffect(() => { + startTracking(); + return () => stopTracking(); + }, [startTracking, stopTracking]); + + const positionClasses = { + 'top-right': 'fixed top-4 right-4', + 'top-left': 'fixed top-4 left-4', + 'bottom-right': 'fixed bottom-4 right-4', + 'bottom-left': 'fixed bottom-4 left-4', + }; + + const getPerformanceScore = () => { + const { webVitals, memoryUsage, averageRenderTime } = performanceData; + + let score = 100; + + // Web Vitals scoring + if (webVitals.lcp > 2500) score -= 20; + else if (webVitals.lcp > 1800) score -= 10; + + if (webVitals.fid > 100) score -= 20; + else if (webVitals.fid > 50) score -= 10; + + if (webVitals.cls > 0.25) score -= 20; + else if (webVitals.cls > 0.1) score -= 10; + + if (webVitals.fcp > 3000) score -= 15; + else if (webVitals.fcp > 1800) score -= 8; + + // Memory scoring + if (memoryUsage > 0.9) score -= 20; + else if (memoryUsage > 0.7) score -= 10; + + // Render time scoring + if (averageRenderTime > 33) score -= 20; + else if (averageRenderTime > 16) score -= 10; + + return Math.max(0, score); + }; + + const getScoreColor = (score: number) => { + if (score >= 90) return 'text-green-600'; + if (score >= 70) return 'text-yellow-600'; + return 'text-red-600'; + }; + + const getScoreBackground = (score: number) => { + if (score >= 90) return 'bg-green-100 dark:bg-green-900/20'; + if (score >= 70) return 'bg-yellow-100 dark:bg-yellow-900/20'; + return 'bg-red-100 dark:bg-red-900/20'; + }; + + const performanceScore = getPerformanceScore(); + const criticalAlerts = performanceData.alerts.filter(alert => alert.severity === 'high'); + const recommendations = getRecommendations(); + + if (!isVisible) { + return ( +
+ +
+ ); + } + + if (minimal) { + return ( +
+
+
+ + + {performanceScore} + + {criticalAlerts.length > 0 && ( + + )} + +
+
+
+ ); + } + + if (showOnlyAlerts && criticalAlerts.length === 0) { + return null; + } + + return ( +
+
+ {/* Header */} +
+
+
+ + + Performance + + + {performanceScore} + +
+
+ + + +
+
+
+ + {/* Critical Alerts */} + {criticalAlerts.length > 0 && ( +
+
+ + + {criticalAlerts.length} Critical Alert{criticalAlerts.length > 1 ? 's' : ''} + +
+
+ {criticalAlerts.slice(0, 3).map(alert => ( +
+ {alert.message} +
+ ))} +
+
+ )} + + {/* Quick Stats */} +
+
+
+
Renders
+
+ {performanceData.componentRenders} +
+
+
+
Avg Time
+
+ {performanceData.averageRenderTime.toFixed(1)}ms +
+
+
+
Memory
+
+ {(performanceData.memoryUsage * 100).toFixed(1)}% +
+
+
+
Bundle
+
+ {(performanceData.bundleSize / 1024).toFixed(0)}KB +
+
+
+
+ + {/* Expanded Content */} + {isExpanded && ( +
+ {/* Web Vitals */} +
+

Core Web Vitals

+
+
+ LCP: + {performanceData.webVitals.lcp.toFixed(0)}ms +
+
+ FID: + {performanceData.webVitals.fid.toFixed(0)}ms +
+
+ CLS: + {performanceData.webVitals.cls.toFixed(3)} +
+
+ FCP: + {performanceData.webVitals.fcp.toFixed(0)}ms +
+
+
+ + {/* Recommendations */} + {recommendations.length > 0 && ( +
+

Recommendations

+
+ {recommendations.slice(0, 3).map((rec, index) => ( +
+ • {rec} +
+ ))} +
+
+ )} +
+ )} + + {/* Actions */} +
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/client/src/hooks/useAdvancedPerformance.ts b/client/src/hooks/useAdvancedPerformance.ts new file mode 100644 index 0000000..0d84840 --- /dev/null +++ b/client/src/hooks/useAdvancedPerformance.ts @@ -0,0 +1,331 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import { performanceMonitor } from '../utils/performance'; +import { bundleAnalyzer } from '../utils/bundleAnalyzer'; + +interface UseAdvancedPerformanceOptions { + trackComponentRenders?: boolean; + trackInteractions?: boolean; + trackMemory?: boolean; + trackWebVitals?: boolean; + enableAlerts?: boolean; + alertThresholds?: { + componentRenderTime?: number; + memoryUsage?: number; + interactionDelay?: number; + }; +} + +interface PerformanceAlert { + id: string; + metric: string; + value: number; + threshold: number; + timestamp: number; + severity: 'low' | 'medium' | 'high'; + message: string; +} + +interface PerformanceData { + componentRenders: number; + averageRenderTime: number; + memoryUsage: number; + interactionCount: number; + webVitals: { + lcp: number; + fid: number; + cls: number; + fcp: number; + ttfb: number; + }; + bundleSize: number; + alerts: PerformanceAlert[]; +} + +export const useAdvancedPerformance = ( + componentName: string, + options: UseAdvancedPerformanceOptions = {} +) => { + const { + trackComponentRenders = true, + trackInteractions = true, + enableAlerts = true, + alertThresholds = { + componentRenderTime: 16, + memoryUsage: 0.8, + interactionDelay: 100, + }, + } = options; + + const [performanceData, setPerformanceData] = useState({ + componentRenders: 0, + averageRenderTime: 0, + memoryUsage: 0, + interactionCount: 0, + webVitals: { + lcp: 0, + fid: 0, + cls: 0, + fcp: 0, + ttfb: 0, + }, + bundleSize: 0, + alerts: [], + }); + + const [isTracking, setIsTracking] = useState(false); + const renderStartTime = useRef(0); + const interactionStartTime = useRef(0); + const alertHistory = useRef([]); + + // Start performance tracking + const startTracking = useCallback(() => { + setIsTracking(true); + + if (trackComponentRenders) { + renderStartTime.current = performance.now(); + } + + // Set up performance alert listener + if (enableAlerts) { + const handlePerformanceAlert = (event: CustomEvent) => { + const alert: PerformanceAlert = { + id: Math.random().toString(36).substr(2, 9), + metric: event.detail.metric, + value: event.detail.value, + threshold: event.detail.budget, + timestamp: event.detail.timestamp, + severity: getSeverity(event.detail.metric, event.detail.value, event.detail.budget), + message: generateAlertMessage(event.detail.metric, event.detail.value, event.detail.budget), + }; + + alertHistory.current.push(alert); + + // Keep only last 20 alerts + if (alertHistory.current.length > 20) { + alertHistory.current = alertHistory.current.slice(-20); + } + + setPerformanceData(prev => ({ + ...prev, + alerts: [...alertHistory.current], + })); + }; + + window.addEventListener('performanceAlert', handlePerformanceAlert as EventListener); + + return () => { + window.removeEventListener('performanceAlert', handlePerformanceAlert as EventListener); + }; + } + }, [trackComponentRenders, enableAlerts]); + + // Stop performance tracking + const stopTracking = useCallback(() => { + setIsTracking(false); + + if (trackComponentRenders && renderStartTime.current > 0) { + const renderTime = performance.now() - renderStartTime.current; + performanceMonitor.measureComponentRender(componentName)(); + + // Check for performance alert + if (enableAlerts && renderTime > alertThresholds.componentRenderTime!) { + const alert: PerformanceAlert = { + id: Math.random().toString(36).substr(2, 9), + metric: 'componentRenderTime', + value: renderTime, + threshold: alertThresholds.componentRenderTime!, + timestamp: Date.now(), + severity: getSeverity('componentRenderTime', renderTime, alertThresholds.componentRenderTime!), + message: `${componentName} render time: ${renderTime.toFixed(2)}ms (threshold: ${alertThresholds.componentRenderTime}ms)`, + }; + + alertHistory.current.push(alert); + } + } + }, [trackComponentRenders, componentName, enableAlerts, alertThresholds]); + + // Track user interaction + const trackInteraction = useCallback((action: string, element?: string) => { + if (!trackInteractions) return; + + const duration = interactionStartTime.current > 0 + ? performance.now() - interactionStartTime.current + : undefined; + + performanceMonitor.trackInteraction(action, element || componentName, duration); + + setPerformanceData(prev => ({ + ...prev, + interactionCount: prev.interactionCount + 1, + })); + + // Check for interaction delay alert + if (enableAlerts && duration && duration > alertThresholds.interactionDelay!) { + const alert: PerformanceAlert = { + id: Math.random().toString(36).substr(2, 9), + metric: 'interactionDelay', + value: duration, + threshold: alertThresholds.interactionDelay!, + timestamp: Date.now(), + severity: getSeverity('interactionDelay', duration, alertThresholds.interactionDelay!), + message: `Interaction delay: ${duration.toFixed(2)}ms (threshold: ${alertThresholds.interactionDelay}ms)`, + }; + + alertHistory.current.push(alert); + } + }, [trackInteractions, componentName, enableAlerts, alertThresholds]); + + // Start interaction timer + const startInteractionTimer = useCallback(() => { + interactionStartTime.current = performance.now(); + }, []); + + // Update performance data + const updatePerformanceData = useCallback(() => { + const summary = performanceMonitor.getPerformanceSummary(); + const bundleAnalysis = bundleAnalyzer.analyzeBundles(); + + setPerformanceData(prev => ({ + ...prev, + componentRenders: summary.totalComponentRenders, + averageRenderTime: summary.averageComponentRenderTime, + memoryUsage: summary.memory.usageRatio, + webVitals: summary.coreWebVitals, + bundleSize: bundleAnalysis.totalSize, + alerts: [...alertHistory.current], + })); + }, []); + + // Get performance recommendations + const getRecommendations = useCallback(() => { + const summary = performanceMonitor.getPerformanceSummary(); + const bundleAnalysis = bundleAnalyzer.analyzeBundles(); + const recommendations: string[] = []; + + // Component recommendations + if (summary.slowComponentsCount > 0) { + recommendations.push(`Optimize ${summary.slowComponentsCount} slow component(s) with render times > 16ms`); + } + + // Memory recommendations + if (summary.memory.usageRatio > 0.8) { + recommendations.push('High memory usage detected. Consider memory optimization techniques'); + } + + // Bundle recommendations + recommendations.push(...bundleAnalysis.recommendations); + + // Web Vitals recommendations + if (summary.coreWebVitals.lcp > 2500) { + recommendations.push('Improve Largest Contentful Paint by optimizing loading performance'); + } + if (summary.coreWebVitals.fid > 100) { + recommendations.push('Reduce First Input Delay by optimizing JavaScript execution'); + } + if (summary.coreWebVitals.cls > 0.1) { + recommendations.push('Reduce Cumulative Layout Shift by ensuring proper element dimensions'); + } + + return recommendations; + }, []); + + // Export performance data + const exportData = useCallback(() => { + const data = { + component: componentName, + timestamp: new Date().toISOString(), + performanceData, + summary: performanceMonitor.getPerformanceSummary(), + bundleAnalysis: bundleAnalyzer.analyzeBundles(), + recommendations: getRecommendations(), + }; + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${componentName}-performance-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, [componentName, performanceData, getRecommendations]); + + // Clear performance data + const clearData = useCallback(() => { + alertHistory.current = []; + performanceMonitor.clearMetrics(); + bundleAnalyzer.clearCache(); + setPerformanceData({ + componentRenders: 0, + averageRenderTime: 0, + memoryUsage: 0, + interactionCount: 0, + webVitals: { + lcp: 0, + fid: 0, + cls: 0, + fcp: 0, + ttfb: 0, + }, + bundleSize: 0, + alerts: [], + }); + }, []); + + // Set up automatic data updates + useEffect(() => { + if (!isTracking) return; + + const interval = setInterval(updatePerformanceData, 2000); // Update every 2 seconds + + return () => clearInterval(interval); + }, [isTracking, updatePerformanceData]); + + // Helper functions + const getSeverity = (_metric: string, value: number, threshold: number): 'low' | 'medium' | 'high' => { + const ratio = value / threshold; + if (ratio <= 1) return 'low'; + if (ratio <= 1.5) return 'medium'; + return 'high'; + }; + + const generateAlertMessage = (metric: string, value: number, threshold: number): string => { + const metricNames: { [key: string]: string } = { + lcp: 'Largest Contentful Paint', + fid: 'First Input Delay', + cls: 'Cumulative Layout Shift', + fcp: 'First Contentful Paint', + ttfb: 'Time to First Byte', + memory: 'Memory Usage', + bundleSize: 'Bundle Size', + }; + + const unit = metric === 'cls' ? '' : metric === 'bundleSize' ? 'KB' : 'ms'; + const formattedValue = metric === 'bundleSize' ? (value / 1024).toFixed(1) : value.toFixed(2); + + return `${metricNames[metric] || metric}: ${formattedValue}${unit} (threshold: ${threshold}${unit})`; + }; + + return { + // State + performanceData, + isTracking, + + // Actions + startTracking, + stopTracking, + trackInteraction, + startInteractionTimer, + updatePerformanceData, + + // Utilities + getRecommendations, + exportData, + clearData, + + // Raw access to monitors + performanceMonitor, + bundleAnalyzer, + }; +}; \ No newline at end of file diff --git a/client/src/pages/TelegramMiniApp.tsx b/client/src/pages/TelegramMiniApp.tsx index 956eee8..0e25b02 100644 --- a/client/src/pages/TelegramMiniApp.tsx +++ b/client/src/pages/TelegramMiniApp.tsx @@ -223,3 +223,5 @@ export const TelegramMiniApp: React.FC = () => {
); }; + +export default TelegramMiniApp; diff --git a/client/src/pages/TelegramSettingsPage.tsx b/client/src/pages/TelegramSettingsPage.tsx index 8ed4f36..87b4fe6 100644 --- a/client/src/pages/TelegramSettingsPage.tsx +++ b/client/src/pages/TelegramSettingsPage.tsx @@ -301,3 +301,5 @@ export const TelegramSettingsPage: React.FC = () => {
); }; + +export default TelegramSettingsPage; diff --git a/client/src/utils/bundleAnalyzer.ts b/client/src/utils/bundleAnalyzer.ts new file mode 100644 index 0000000..d63e5c5 --- /dev/null +++ b/client/src/utils/bundleAnalyzer.ts @@ -0,0 +1,249 @@ +interface BundleInfo { + name: string; + size: number; + chunks: string[]; + dependencies: string[]; + loadTime: number; +} + +interface BundleAnalysis { + totalSize: number; + bundles: BundleInfo[]; + recommendations: string[]; + largestBundles: BundleInfo[]; + optimizationOpportunities: string[]; +} + +class BundleAnalyzer { + private bundleCache: Map = new Map(); + private analysisHistory: BundleAnalysis[] = []; + + constructor() { + this.trackInitialBundles(); + } + + private trackInitialBundles() { + if ('performance' in window && 'getEntriesByType' in performance) { + const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; + + entries.forEach(entry => { + if (this.isJavaScriptBundle(entry.name)) { + this.trackBundle(entry.name, entry.transferSize || 0, entry.duration); + } + }); + } + } + + private isJavaScriptBundle(url: string): boolean { + return url.includes('.js') && !url.includes('node_modules') && !url.includes('hot-update'); + } + + trackBundle(name: string, size: number, loadTime: number) { + const bundleInfo: BundleInfo = { + name: name.split('/').pop() || name, + size, + chunks: this.extractChunks(name), + dependencies: this.extractDependencies(name), + loadTime, + }; + + this.bundleCache.set(bundleInfo.name, bundleInfo); + + // Trigger performance monitoring + if (window.performanceMonitor) { + window.performanceMonitor.trackBundleLoad(bundleInfo.name, size, loadTime); + } + } + + private extractChunks(name: string): string[] { + // Extract chunk information from bundle name + const chunkMatch = name.match(/(.+)-(\w+)\.js$/); + if (chunkMatch) { + return [chunkMatch[1]]; + } + return ['main']; + } + + private extractDependencies(name: string): string[] { + // This would typically be analyzed during build time + // For now, return common dependencies based on naming patterns + const dependencies: string[] = []; + + if (name.includes('vendor')) { + dependencies.push('vendor-libraries'); + } + if (name.includes('react')) { + dependencies.push('react', 'react-dom'); + } + if (name.includes('router')) { + dependencies.push('react-router-dom'); + } + + return dependencies; + } + + analyzeBundles(): BundleAnalysis { + const bundles = Array.from(this.bundleCache.values()); + const totalSize = bundles.reduce((sum, bundle) => sum + bundle.size, 0); + + // Sort bundles by size + const sortedBundles = bundles.sort((a, b) => b.size - a.size); + const largestBundles = sortedBundles.slice(0, 5); + + // Generate recommendations + const recommendations = this.generateRecommendations(bundles, totalSize); + const optimizationOpportunities = this.identifyOptimizationOpportunities(bundles); + + const analysis: BundleAnalysis = { + totalSize, + bundles: sortedBundles, + recommendations, + largestBundles, + optimizationOpportunities, + }; + + this.analysisHistory.push(analysis); + + // Keep only last 10 analyses + if (this.analysisHistory.length > 10) { + this.analysisHistory = this.analysisHistory.slice(-10); + } + + return analysis; + } + + private generateRecommendations(bundles: BundleInfo[], totalSize: number): string[] { + const recommendations: string[] = []; + + // Check for large bundles + const largeBundles = bundles.filter(bundle => bundle.size > 250 * 1024); // 250KB + if (largeBundles.length > 0) { + recommendations.push(`Consider code splitting for ${largeBundles.length} large bundle(s) over 250KB`); + } + + // Check total bundle size + if (totalSize > 1024 * 1024) { // 1MB + recommendations.push('Total bundle size exceeds 1MB. Consider tree shaking and removing unused dependencies'); + } + + // Check for duplicate dependencies + const duplicateDeps = this.findDuplicateDependencies(bundles); + if (duplicateDeps.length > 0) { + recommendations.push(`Found duplicate dependencies: ${duplicateDeps.join(', ')}. Consider vendor chunk optimization`); + } + + // Check load times + const slowBundles = bundles.filter(bundle => bundle.loadTime > 200); // 200ms + if (slowBundles.length > 0) { + recommendations.push(`${slowBundles.length} bundle(s) taking longer than 200ms to load. Consider compression or CDN`); + } + + if (recommendations.length === 0) { + recommendations.push('Bundle sizes are within acceptable limits'); + } + + return recommendations; + } + + private findDuplicateDependencies(bundles: BundleInfo[]): string[] { + const dependencyCount = new Map(); + + bundles.forEach(bundle => { + bundle.dependencies.forEach(dep => { + dependencyCount.set(dep, (dependencyCount.get(dep) || 0) + 1); + }); + }); + + return Array.from(dependencyCount.entries()) + .filter(([_, count]) => count > 1) + .map(([dep, _]) => dep); + } + + private identifyOptimizationOpportunities(bundles: BundleInfo[]): string[] { + const opportunities: string[] = []; + + // Check for lazy loading opportunities + const lazyLoadCandidates = bundles.filter(bundle => + bundle.name.includes('admin') || + bundle.name.includes('analytics') || + bundle.name.includes('settings') + ); + + if (lazyLoadCandidates.length > 0) { + opportunities.push(`Consider lazy loading ${lazyLoadCandidates.length} feature-specific bundle(s)`); + } + + // Check for vendor optimization + const vendorBundles = bundles.filter(bundle => bundle.name.includes('vendor')); + if (vendorBundles.length > 1) { + opportunities.push('Multiple vendor bundles detected. Consider consolidating into a single vendor chunk'); + } + + // Check for image optimization + const imageHeavyBundles = bundles.filter(bundle => bundle.size > 100 * 1024); + if (imageHeavyBundles.length > 0) { + opportunities.push('Consider image optimization and lazy loading for large bundles'); + } + + return opportunities; + } + + getBundleAnalysisHistory(): BundleAnalysis[] { + return this.analysisHistory; + } + + clearCache() { + this.bundleCache.clear(); + this.analysisHistory = []; + } + + // Export bundle analysis for external analysis + exportAnalysis(): string { + const analysis = this.analyzeBundles(); + return JSON.stringify({ + timestamp: new Date().toISOString(), + analysis, + history: this.analysisHistory, + }, null, 2); + } + + // Get bundle size distribution + getSizeDistribution(): { [key: string]: number } { + const bundles = Array.from(this.bundleCache.values()); + const distribution: { [key: string]: number } = { + small: 0, // < 50KB + medium: 0, // 50KB - 150KB + large: 0, // 150KB - 250KB + xlarge: 0, // > 250KB + }; + + bundles.forEach(bundle => { + if (bundle.size < 50 * 1024) { + distribution.small++; + } else if (bundle.size < 150 * 1024) { + distribution.medium++; + } else if (bundle.size < 250 * 1024) { + distribution.large++; + } else { + distribution.xlarge++; + } + }); + + return distribution; + } +} + +// Create singleton instance +export const bundleAnalyzer = new BundleAnalyzer(); + +// Make it globally available for performance monitoring +declare global { + interface Window { + bundleAnalyzer?: BundleAnalyzer; + performanceMonitor?: any; + } +} + +if (typeof window !== 'undefined') { + window.bundleAnalyzer = bundleAnalyzer; +} \ No newline at end of file diff --git a/client/src/utils/performance.tsx b/client/src/utils/performance.tsx index 7c571cf..f07b3a9 100644 --- a/client/src/utils/performance.tsx +++ b/client/src/utils/performance.tsx @@ -21,22 +21,60 @@ interface UserInteractionMetrics { timestamp: number; } +interface CoreWebVitals { + lcp: number; // Largest Contentful Paint + fid: number; // First Input Delay + cls: number; // Cumulative Layout Shift + fcp: number; // First Contentful Paint + ttfb: number; // Time to First Byte +} + +interface BundleMetrics { + name: string; + size: number; + loadTime: number; + timestamp: number; +} + +interface MemoryMetrics { + usedJSHeapSize: number; + totalJSHeapSize: number; + jsHeapSizeLimit: number; + timestamp: number; +} + class PerformanceMonitor { private metrics: { components: PerformanceMetrics[]; apis: ApiMetrics[]; interactions: UserInteractionMetrics[]; + coreWebVitals: CoreWebVitals[]; + bundles: BundleMetrics[]; + memory: MemoryMetrics[]; } = { components: [], apis: [], interactions: [], + coreWebVitals: [], + bundles: [], + memory: [], }; private observers: PerformanceObserver[] = []; + private performanceBudgets = { + componentRender: 16, // ms + apiCall: 1000, // ms + lcp: 2500, // ms + fid: 100, // ms + cls: 0.1, + bundleSize: 250 * 1024, // 250KB + }; constructor() { this.initializeObservers(); this.setupNavigationTiming(); + this.setupCoreWebVitals(); + this.setupMemoryMonitoring(); } private initializeObservers() { @@ -122,6 +160,162 @@ class PerformanceMonitor { }); } + private setupCoreWebVitals() { + // Largest Contentful Paint (LCP) + if ('PerformanceObserver' in window) { + try { + const lcpObserver = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1]; + if (lastEntry) { + this.recordWebVital('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 (FID) + try { + const fidObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (entry.entryType === 'first-input') { + const fidEntry = entry as any; + this.recordWebVital('fid', fidEntry.processingStart - fidEntry.startTime); + } + } + }); + fidObserver.observe({ entryTypes: ['first-input'] }); + this.observers.push(fidObserver); + } catch (error) { + console.warn('FID observer not supported:', error); + } + + // Cumulative Layout Shift (CLS) + try { + let clsValue = 0; + const clsObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (!(entry as any).hadRecentInput) { + clsValue += (entry as any).value; + } + } + this.recordWebVital('cls', clsValue); + }); + clsObserver.observe({ entryTypes: ['layout-shift'] }); + this.observers.push(clsObserver); + } catch (error) { + console.warn('CLS observer not supported:', error); + } + } + + // First Contentful Paint (FCP) and Time to First Byte (TTFB) + this.measurePaintMetrics(); + } + + private measurePaintMetrics() { + if ('performance' in window && 'getEntriesByType' in performance) { + const paintEntries = performance.getEntriesByType('paint'); + const navigationEntries = performance.getEntriesByType('navigation'); + + paintEntries.forEach((entry) => { + if (entry.name === 'first-contentful-paint') { + this.recordWebVital('fcp', entry.startTime); + } + }); + + if (navigationEntries.length > 0) { + const navEntry = navigationEntries[0] as PerformanceNavigationTiming; + this.recordWebVital('ttfb', navEntry.responseStart - navEntry.requestStart); + } + } + } + + private recordWebVital(name: keyof CoreWebVitals, value: number) { + const vital: CoreWebVitals = { + lcp: 0, + fid: 0, + cls: 0, + fcp: 0, + ttfb: 0, + [name]: value, + }; + + this.metrics.coreWebVitals.push(vital); + + // Check against budgets and log warnings + const budget = this.performanceBudgets[name as keyof typeof this.performanceBudgets]; + if (budget && value > budget) { + console.warn(`Performance budget exceeded for ${name}: ${value.toFixed(2)} > ${budget}`); + this.triggerPerformanceAlert(name, value, budget); + } + } + + private setupMemoryMonitoring() { + if ('memory' in performance) { + const recordMemory = () => { + const memory = (performance as any).memory; + const metric: MemoryMetrics = { + usedJSHeapSize: memory.usedJSHeapSize, + totalJSHeapSize: memory.totalJSHeapSize, + jsHeapSizeLimit: memory.jsHeapSizeLimit, + timestamp: Date.now(), + }; + this.metrics.memory.push(metric); + + // Alert if memory usage is high (> 80% of limit) + const usageRatio = memory.usedJSHeapSize / memory.jsHeapSizeLimit; + if (usageRatio > 0.8) { + console.warn(`High memory usage: ${(usageRatio * 100).toFixed(1)}%`); + this.triggerPerformanceAlert('memory', usageRatio, 0.8); + } + }; + + // Record memory every 10 seconds + setInterval(recordMemory, 10000); + recordMemory(); // Initial recording + } + } + + private triggerPerformanceAlert(metric: string, value: number, budget: number) { + const alert = { + metric, + value, + budget, + timestamp: Date.now(), + url: window.location.pathname, + }; + + // Store alert for dashboard + this.metrics.interactions.push({ + action: 'performance_alert', + element: `${metric}_alert`, + duration: value, + timestamp: alert.timestamp, + }); + + // Dispatch custom event for real-time monitoring + window.dispatchEvent(new CustomEvent('performanceAlert', { detail: alert })); + } + + // Bundle size monitoring + trackBundleLoad(bundleName: string, size: number, loadTime: number) { + const metric: BundleMetrics = { + name: bundleName, + size, + loadTime, + timestamp: Date.now(), + }; + this.metrics.bundles.push(metric); + + if (size > this.performanceBudgets.bundleSize) { + console.warn(`Bundle size exceeded: ${bundleName} ${(size / 1024).toFixed(1)}KB`); + this.triggerPerformanceAlert('bundleSize', size, this.performanceBudgets.bundleSize); + } + } + // Component performance monitoring measureComponentRender(componentName: string) { const startTime = performance.now(); @@ -215,6 +409,26 @@ class PerformanceMonitor { const slowComponents = this.metrics.components.filter(m => m.componentRenderTime > 16); const slowApis = this.metrics.apis.filter(m => m.duration > 1000); + // Core Web Vitals averages + const lcpAvg = this.metrics.coreWebVitals.filter(v => v.lcp > 0).reduce((sum, v) => sum + v.lcp, 0) / this.metrics.coreWebVitals.filter(v => v.lcp > 0).length || 0; + const fidAvg = this.metrics.coreWebVitals.filter(v => v.fid > 0).reduce((sum, v) => sum + v.fid, 0) / this.metrics.coreWebVitals.filter(v => v.fid > 0).length || 0; + const clsAvg = this.metrics.coreWebVitals.filter(v => v.cls > 0).reduce((sum, v) => sum + v.cls, 0) / this.metrics.coreWebVitals.filter(v => v.cls > 0).length || 0; + const fcpAvg = this.metrics.coreWebVitals.filter(v => v.fcp > 0).reduce((sum, v) => sum + v.fcp, 0) / this.metrics.coreWebVitals.filter(v => v.fcp > 0).length || 0; + const ttfbAvg = this.metrics.coreWebVitals.filter(v => v.ttfb > 0).reduce((sum, v) => sum + v.ttfb, 0) / this.metrics.coreWebVitals.filter(v => v.ttfb > 0).length || 0; + + // Bundle metrics + const totalBundleSize = this.metrics.bundles.reduce((sum, b) => sum + b.size, 0); + const avgBundleLoadTime = this.metrics.bundles.length > 0 + ? this.metrics.bundles.reduce((sum, b) => sum + b.loadTime, 0) / this.metrics.bundles.length + : 0; + + // Memory metrics + const currentMemory = this.metrics.memory[this.metrics.memory.length - 1]; + const memoryUsageRatio = currentMemory ? currentMemory.usedJSHeapSize / currentMemory.jsHeapSizeLimit : 0; + + // Performance alerts + const performanceAlerts = this.metrics.interactions.filter(i => i.action === 'performance_alert'); + return { totalComponentRenders: this.metrics.components.length, averageComponentRenderTime: componentAvg, @@ -226,6 +440,28 @@ class PerformanceMonitor { apiSuccessRate: this.metrics.apis.length > 0 ? (this.metrics.apis.filter(m => m.success).length / this.metrics.apis.length) * 100 : 100, + // Core Web Vitals + coreWebVitals: { + lcp: lcpAvg, + fid: fidAvg, + cls: clsAvg, + fcp: fcpAvg, + ttfb: ttfbAvg, + }, + // Bundle metrics + bundles: { + totalSize: totalBundleSize, + averageLoadTime: avgBundleLoadTime, + count: this.metrics.bundles.length, + }, + // Memory metrics + memory: { + usageRatio: memoryUsageRatio, + usedHeapMB: currentMemory ? currentMemory.usedJSHeapSize / (1024 * 1024) : 0, + totalHeapMB: currentMemory ? currentMemory.totalJSHeapSize / (1024 * 1024) : 0, + }, + // Performance alerts + performanceAlertsCount: performanceAlerts.length, }; } @@ -235,6 +471,9 @@ class PerformanceMonitor { components: [], apis: [], interactions: [], + coreWebVitals: [], + bundles: [], + memory: [], }; } diff --git a/client/vite.config.ts b/client/vite.config.ts index 2103e67..dd69fdd 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -70,6 +70,66 @@ export default defineConfig({ }, }), ], + build: { + rollupOptions: { + output: { + manualChunks: { + // Vendor chunks for better caching + 'vendor-react': ['react', 'react-dom'], + 'vendor-router': ['react-router-dom'], + 'vendor-ui': ['lucide-react', 'react-hot-toast'], + 'vendor-sockets': ['socket.io-client'], + 'vendor-utils': ['axios', 'date-fns', 'clsx'], + 'vendor-media': ['wavesurfer.js', 'react-player', 'browser-image-compression'], + 'vendor-emoji': ['emoji-picker-react'], + + // Feature-based chunks + 'auth-features': [ + './src/pages/LoginPage.tsx', + './src/pages/RegisterPage.tsx', + './src/pages/VerifyEmailPage.tsx' + ], + 'chat-features': [ + './src/components/ChatWindow.tsx', + './src/components/ChatList.tsx' + ], + 'media-features': [ + './src/components/VoiceRecorder.tsx', + './src/components/MediaViewer.tsx', + './src/components/AudioVisualizer.tsx' + ], + 'admin-features': [ + './src/components/UserSettings.tsx', + './src/components/BotManager.tsx', + './src/components/AnalyticsDashboard.tsx' + ] + }, + chunkFileNames: (chunkInfo) => { + const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop() : 'chunk'; + return `js/[name]-[hash].js`; + }, + entryFileNames: 'js/[name]-[hash].js', + assetFileNames: (assetInfo) => { + const info = assetInfo.name.split('.'); + const ext = info[info.length - 1]; + if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) { + return `media/[name]-[hash][extname]`; + } + if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i.test(assetInfo.name)) { + return `images/[name]-[hash][extname]`; + } + if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) { + return `fonts/[name]-[hash][extname]`; + } + return `assets/[name]-[hash][extname]`; + }, + }, + }, + target: 'esnext', + minify: 'terser', + sourcemap: true, + chunkSizeWarningLimit: 1000, + }, resolve: { alias: { '@': path.resolve(__dirname, './src'),