diff --git a/backend/src/middleware/performanceHeaders.js b/backend/src/middleware/performanceHeaders.js new file mode 100644 index 00000000..181c8444 --- /dev/null +++ b/backend/src/middleware/performanceHeaders.js @@ -0,0 +1,56 @@ +const performanceHeaders = (req, res, next) => { + // Security and performance headers + const headers = { + // Caching headers + 'Cache-Control': 'public, max-age=31536000, immutable', // 1 year for static assets + 'ETag': 'W/"v1"', // Weak ETag for better caching + + // Compression headers + 'Content-Encoding': 'gzip', + 'Vary': 'Accept-Encoding', + + // Browser optimization headers + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + + // Performance hints + 'Permissions-Policy': 'geolocation=(), microphone=(), camera=()', + 'Feature-Policy': 'geolocation \'none\'; microphone \'none\'; camera \'none\'', + + // Preload critical resources + 'Link': [ + '; rel=preload; as=script', + '; rel=preload; as=style' + ].join(', ') + }; + + // Apply different caching strategies based on content type + const contentType = req.get('Content-Type') || ''; + + if (contentType.includes('image/')) { + // Images - cache for 1 year + res.set('Cache-Control', 'public, max-age=31536000, immutable'); + } else if (contentType.includes('text/css') || contentType.includes('application/javascript')) { + // CSS/JS - cache for 1 year with validation + res.set('Cache-Control', 'public, max-age=31536000, must-revalidate'); + } else if (contentType.includes('text/html')) { + // HTML - shorter cache for dynamic content + res.set('Cache-Control', 'public, max-age=3600, must-revalidate'); // 1 hour + } else { + // Default caching + res.set('Cache-Control', 'public, max-age=86400'); // 1 day + } + + // Apply common headers + Object.entries(headers).forEach(([key, value]) => { + if (!res.get(key)) { + res.set(key, value); + } + }); + + next(); +}; + +module.exports = performanceHeaders; diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 00000000..7198650c --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,16 @@ +# Performance optimizations +GENERATE_SOURCEMAP=false +INLINE_RUNTIME_CHUNK=false +IMAGE_INLINE_SIZE_LIMIT=10000 + +# Bundle analyzer +BUNDLE_ANALYZER=false + +# Compression +COMPRESSION=true + +# Cache settings +SERVICE_WORKER=true + +# Build optimizations +REACT_APP_BUILD_OPTIMIZED=true diff --git a/frontend/config/webpack.config.js b/frontend/config/webpack.config.js new file mode 100644 index 00000000..9732e129 --- /dev/null +++ b/frontend/config/webpack.config.js @@ -0,0 +1,96 @@ +const TerserPlugin = require('terser-webpack-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); +const path = require('path'); + +module.exports = { + webpack: function(config, env) { + // Production optimizations + if (env === 'production') { + // Minify JavaScript + config.optimization.minimizer.push( + new TerserPlugin({ + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + format: { + comments: false, + }, + }, + extractComments: false, + }) + ); + + // Minify CSS + config.optimization.minimizer.push( + new CssMinimizerPlugin({ + minimizerOptions: { + preset: [ + 'default', + { + discardComments: { removeAll: true }, + normalizeWhitespace: true, + }, + ], + }, + }) + ); + + // Add compression for assets + config.plugins.push( + new CompressionPlugin({ + algorithm: 'gzip', + test: /\.(js|css|html|svg)$/, + threshold: 8192, + minRatio: 0.8, + }) + ); + + // Image optimization + config.module.rules.push({ + test: /\.(jpe?g|png|gif|webp)$/i, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[contenthash].[ext]', + outputPath: 'images/', + }, + }, + { + loader: 'img-optimize-loader', + options: { + gifsicle: { optimizationLevel: 7 }, + mozjpeg: { quality: 85 }, + pngquant: { quality: [0.65, 0.8] }, + svgo: { + plugins: [ + { removeViewBox: false }, + { removeEmptyAttrs: false }, + ], + }, + }, + }, + ], + }); + } + + // Add lazy loading for images + config.module.rules.push({ + test: /\.(jpe?g|png|gif|webp)$/i, + use: [ + { + loader: 'lazy-image-loader', + options: { + placeholder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==', + threshold: 100, + }, + }, + ], + }); + + return config; + }, +}; diff --git a/frontend/package.json b/frontend/package.json index 46c79df0..8396c1c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,10 +22,13 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", + "build:analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint src/", - "lint:fix": "eslint src/ --fix" + "lint:fix": "eslint src/ --fix", + "optimize": "npm run build && npm run compress:images", + "compress:images": "node scripts/compress-images.js" }, "devDependencies": { "react-scripts": "5.0.1", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index bc3562b3..b7a28f60 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -13,7 +13,10 @@ const STATIC_ASSETS = [ '/static/js/main.js', // Add icons '/icons/icon-192x192.png', - '/icons/icon-512x512.png' + '/icons/icon-512x512.png', + '/logo192.png', + '/logo512.png', + '/favicon.ico' ]; // API endpoints that should be cached diff --git a/frontend/scripts/compress-images.js b/frontend/scripts/compress-images.js new file mode 100644 index 00000000..990fcfec --- /dev/null +++ b/frontend/scripts/compress-images.js @@ -0,0 +1,36 @@ +const imagemin = require('imagemin'); +const imageminMozjpeg = require('imagemin-mozjpeg'); +const imageminPngquant = require('imagemin-pngquant'); +const path = require('path'); + +const compressImages = async () => { + try { + console.log('🖼️ Starting image compression...'); + + const files = await imagemin(['public/**/*.{jpg,jpeg,png}'], { + destination: 'public/optimized', + plugins: [ + imageminMozjpeg({ quality: 85, progressive: true }), + imageminPngquant({ quality: [0.65, 0.8], speed: 4 }) + ] + }); + + console.log(`✅ Compressed ${files.length} images`); + console.log('📊 Compression results:'); + files.forEach(file => { + const originalSize = file.sourcePath ? + require('fs').statSync(file.sourcePath).size : 0; + const compressedSize = file.data.length; + const savings = originalSize > 0 ? + ((originalSize - compressedSize) / originalSize * 100).toFixed(2) : 0; + + console.log(` ${path.basename(file.destinationPath)}: ${savings}% reduction`); + }); + + } catch (error) { + console.error('❌ Error compressing images:', error); + process.exit(1); + } +}; + +compressImages(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aed60433..0908a013 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from 'react-query'; import { Toaster } from 'react-hot-toast'; @@ -12,11 +12,34 @@ import Marketplace from './pages/Marketplace'; import Search from './pages/Search'; import WalletDemo from './pages/WalletDemo'; import RouteChangeTracker from './analytics/RouteChangeTracker'; +import { performanceMetrics, preloadCriticalResources } from './utils/performance'; import './App.css'; const queryClient = new QueryClient(); function App() { + useEffect(() => { + // Initialize performance optimizations + preloadCriticalResources(); + performanceMetrics.lazyLoadImages(); + + // Measure page load performance + const metrics = performanceMetrics.measurePageLoad(); + if (metrics) { + console.log('[Performance] Page load metrics:', metrics); + } + + // Monitor memory usage periodically + const memoryInterval = setInterval(() => { + const memory = performanceMetrics.getMemoryUsage(); + if (memory && memory.used / memory.total > 0.8) { + console.warn('[Performance] High memory usage detected:', memory); + } + }, 30000); // Check every 30 seconds + + return () => clearInterval(memoryInterval); + }, []); + return ( diff --git a/frontend/src/components/LazyImage.tsx b/frontend/src/components/LazyImage.tsx new file mode 100644 index 00000000..e2b89f2c --- /dev/null +++ b/frontend/src/components/LazyImage.tsx @@ -0,0 +1,84 @@ +import React, { useState, useRef, useEffect } from 'react'; + +interface LazyImageProps { + src: string; + alt: string; + className?: string; + placeholder?: string; + threshold?: number; + onLoad?: () => void; + onError?: () => void; +} + +const LazyImage: React.FC = ({ + src, + alt, + className = '', + placeholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==', + threshold = 100, + onLoad, + onError +}) => { + const [isLoaded, setIsLoaded] = useState(false); + const [isInView, setIsInView] = useState(false); + const [hasError, setHasError] = useState(false); + const imgRef = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsInView(true); + observer.disconnect(); + } + }, + { + rootMargin: `${threshold}px`, + } + ); + + if (imgRef.current) { + observer.observe(imgRef.current); + } + + return () => observer.disconnect(); + }, [threshold]); + + const handleLoad = () => { + setIsLoaded(true); + onLoad?.(); + }; + + const handleError = () => { + setHasError(true); + onError?.(); + }; + + return ( +
+ {alt} + + {!isLoaded && !hasError && ( +
+ )} + + {hasError && ( +
+ Failed to load image +
+ )} +
+ ); +}; + +export default LazyImage; diff --git a/frontend/src/components/Media/VideoRecorder.tsx b/frontend/src/components/Media/VideoRecorder.tsx index b7c9f658..f35310d7 100644 --- a/frontend/src/components/Media/VideoRecorder.tsx +++ b/frontend/src/components/Media/VideoRecorder.tsx @@ -463,6 +463,7 @@ const VideoRecorder: React.FC = ({ muted={recordingState === 'recording'} className="w-full h-full object-cover" onPlay={applyWatermark} + loading="lazy" /> {/* Watermark Canvas (overlay) */} @@ -500,6 +501,7 @@ const VideoRecorder: React.FC = ({ src={URL.createObjectURL(recordedBlob)} controls className="w-full h-full" + loading="lazy" /> )} diff --git a/frontend/src/utils/performance.ts b/frontend/src/utils/performance.ts new file mode 100644 index 00000000..4b81513c --- /dev/null +++ b/frontend/src/utils/performance.ts @@ -0,0 +1,120 @@ +// Performance monitoring utilities +export const performanceMetrics = { + // Measure page load time + measurePageLoad: () => { + if (typeof window !== 'undefined' && window.performance) { + const navigation = window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; + return { + domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, + loadComplete: navigation.loadEventEnd - navigation.loadEventStart, + firstPaint: window.performance.getEntriesByType('paint').find(entry => entry.name === 'first-paint')?.startTime || 0, + firstContentfulPaint: window.performance.getEntriesByType('paint').find(entry => entry.name === 'first-contentful-paint')?.startTime || 0, + }; + } + return null; + }, + + // Measure component render time + measureComponentRender: (componentName: string) => { + const startTime = performance.now(); + return () => { + const endTime = performance.now(); + console.log(`[Performance] ${componentName} rendered in ${endTime - startTime}ms`); + }; + }, + + // Debounce function for performance optimization + debounce: any>( + func: T, + wait: number + ): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout; + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; + }, + + // Throttle function for performance optimization + throttle: any>( + func: T, + limit: number + ): ((...args: Parameters) => void) => { + let inThrottle: boolean; + return (...args: Parameters) => { + if (!inThrottle) { + func(...args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + // Lazy load images with intersection observer + lazyLoadImages: () => { + if ('IntersectionObserver' in window) { + const imageObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target as HTMLImageElement; + if (img.dataset.src) { + img.src = img.dataset.src; + img.classList.remove('lazy'); + imageObserver.unobserve(img); + } + } + }); + }); + + document.querySelectorAll('img[data-src]').forEach(img => { + imageObserver.observe(img); + }); + } + }, + + // Preload critical resources + preloadResource: (url: string, as: string) => { + const link = document.createElement('link'); + link.rel = 'preload'; + link.href = url; + link.as = as; + document.head.appendChild(link); + }, + + // Monitor memory usage + getMemoryUsage: () => { + if ('memory' in performance) { + return { + used: (performance as any).memory.usedJSHeapSize, + total: (performance as any).memory.totalJSHeapSize, + limit: (performance as any).memory.jsHeapSizeLimit, + }; + } + return null; + }, +}; + +// Web Vitals monitoring +export const reportWebVitals = (onPerfEntry?: (metric: any) => void) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +// Critical resource preloader +export const preloadCriticalResources = () => { + const criticalResources = [ + { url: '/static/css/main.css', as: 'style' }, + { url: '/static/js/main.js', as: 'script' }, + ]; + + criticalResources.forEach(resource => { + performanceMetrics.preloadResource(resource.url, resource.as); + }); +};