Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions backend/src/middleware/performanceHeaders.js
Original file line number Diff line number Diff line change
@@ -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': [
'</static/js/main.js>; rel=preload; as=script',
'</static/css/main.css>; 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;
16 changes: 16 additions & 0 deletions frontend/.env.production
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions frontend/config/webpack.config.js
Original file line number Diff line number Diff line change
@@ -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;
},
};
5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion frontend/public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions frontend/scripts/compress-images.js
Original file line number Diff line number Diff line change
@@ -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();
25 changes: 24 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<QueryClientProvider client={queryClient}>
<Router>
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/components/LazyImage.tsx
Original file line number Diff line number Diff line change
@@ -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<LazyImageProps> = ({
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<HTMLImageElement>(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 (
<div className={`relative overflow-hidden ${className}`}>
<img
ref={imgRef}
src={isInView ? src : placeholder}
alt={alt}
loading="lazy"
className={`transition-opacity duration-300 ${
isLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={handleLoad}
onError={handleError}
/>

{!isLoaded && !hasError && (
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
)}

{hasError && (
<div className="absolute inset-0 bg-gray-300 flex items-center justify-center">
<span className="text-gray-500 text-sm">Failed to load image</span>
</div>
)}
</div>
);
};

export default LazyImage;
2 changes: 2 additions & 0 deletions frontend/src/components/Media/VideoRecorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ const VideoRecorder: React.FC<VideoRecorderProps> = ({
muted={recordingState === 'recording'}
className="w-full h-full object-cover"
onPlay={applyWatermark}
loading="lazy"
/>

{/* Watermark Canvas (overlay) */}
Expand Down Expand Up @@ -500,6 +501,7 @@ const VideoRecorder: React.FC<VideoRecorderProps> = ({
src={URL.createObjectURL(recordedBlob)}
controls
className="w-full h-full"
loading="lazy"
/>
)}

Expand Down
Loading
Loading