feat(performance): enhance application performance with optimizations and monitoring
- Updated package dependencies for improved localization support and performance. - Introduced CSS performance optimizations to prevent layout shifts and enhance rendering efficiency. - Implemented asset preloading and lazy loading strategies for critical components to improve load times. - Enhanced translation loading with optimized caching and background loading strategies. - Added performance monitoring utilities to track key metrics and improve user experience. - Refactored task management components to utilize new performance features and ensure efficient rendering. - Introduced new utility functions for asset and CSS optimizations to streamline resource management.
This commit is contained in:
588
worklenz-frontend/src/utils/asset-optimizations.ts
Normal file
588
worklenz-frontend/src/utils/asset-optimizations.ts
Normal file
@@ -0,0 +1,588 @@
|
||||
// Asset optimization utilities for improved performance
|
||||
|
||||
// Image optimization constants
|
||||
export const IMAGE_OPTIMIZATION = {
|
||||
// Quality settings for different use cases
|
||||
QUALITY: {
|
||||
THUMBNAIL: 70,
|
||||
AVATAR: 80,
|
||||
CONTENT: 85,
|
||||
HIGH_QUALITY: 95,
|
||||
},
|
||||
|
||||
// Size presets for responsive images
|
||||
SIZES: {
|
||||
THUMBNAIL: { width: 64, height: 64 },
|
||||
AVATAR_SMALL: { width: 32, height: 32 },
|
||||
AVATAR_MEDIUM: { width: 48, height: 48 },
|
||||
AVATAR_LARGE: { width: 64, height: 64 },
|
||||
ICON_SMALL: { width: 16, height: 16 },
|
||||
ICON_MEDIUM: { width: 24, height: 24 },
|
||||
ICON_LARGE: { width: 32, height: 32 },
|
||||
CARD_IMAGE: { width: 300, height: 200 },
|
||||
},
|
||||
|
||||
// Supported formats in order of preference
|
||||
FORMATS: ['webp', 'jpeg', 'png'],
|
||||
|
||||
// Browser support detection
|
||||
WEBP_SUPPORT: typeof window !== 'undefined' &&
|
||||
window.document?.createElement('canvas').toDataURL('image/webp').indexOf('webp') > -1,
|
||||
} as const;
|
||||
|
||||
// Asset caching strategies
|
||||
export const CACHE_STRATEGIES = {
|
||||
// Cache durations in seconds
|
||||
DURATIONS: {
|
||||
STATIC_ASSETS: 31536000, // 1 year
|
||||
IMAGES: 2592000, // 30 days
|
||||
AVATARS: 86400, // 1 day
|
||||
DYNAMIC_CONTENT: 3600, // 1 hour
|
||||
},
|
||||
|
||||
// Cache keys
|
||||
KEYS: {
|
||||
COMPRESSED_IMAGES: 'compressed_images',
|
||||
AVATAR_CACHE: 'avatar_cache',
|
||||
ICON_CACHE: 'icon_cache',
|
||||
STATIC_ASSETS: 'static_assets',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Image compression utilities
|
||||
export class ImageOptimizer {
|
||||
private static canvas: HTMLCanvasElement | null = null;
|
||||
private static ctx: CanvasRenderingContext2D | null = null;
|
||||
|
||||
private static getCanvas(): HTMLCanvasElement {
|
||||
if (!this.canvas) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
}
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
// Compress image with quality and size options
|
||||
static async compressImage(
|
||||
file: File | string,
|
||||
options: {
|
||||
quality?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
format?: 'jpeg' | 'webp' | 'png';
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
const {
|
||||
quality = IMAGE_OPTIMIZATION.QUALITY.CONTENT,
|
||||
maxWidth = 1920,
|
||||
maxHeight = 1080,
|
||||
format = 'jpeg',
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = this.getCanvas();
|
||||
const ctx = this.ctx!;
|
||||
|
||||
// Calculate optimal dimensions
|
||||
const { width, height } = this.calculateOptimalSize(
|
||||
img.width,
|
||||
img.height,
|
||||
maxWidth,
|
||||
maxHeight
|
||||
);
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Clear canvas and draw resized image
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Convert to optimized format
|
||||
const mimeType = format === 'jpeg' ? 'image/jpeg' :
|
||||
format === 'webp' ? 'image/webp' : 'image/png';
|
||||
|
||||
const compressedDataUrl = canvas.toDataURL(mimeType, quality / 100);
|
||||
resolve(compressedDataUrl);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = reject;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
img.src = file;
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate optimal size maintaining aspect ratio
|
||||
private static calculateOptimalSize(
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
maxWidth: number,
|
||||
maxHeight: number
|
||||
): { width: number; height: number } {
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
|
||||
let width = originalWidth;
|
||||
let height = originalHeight;
|
||||
|
||||
// Scale down if necessary
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
height = width / aspectRatio;
|
||||
}
|
||||
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
width = height * aspectRatio;
|
||||
}
|
||||
|
||||
return {
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
};
|
||||
}
|
||||
|
||||
// Generate responsive image srcSet
|
||||
static generateSrcSet(
|
||||
baseUrl: string,
|
||||
sizes: Array<{ width: number; quality?: number }>
|
||||
): string {
|
||||
return sizes
|
||||
.map(({ width, quality = IMAGE_OPTIMIZATION.QUALITY.CONTENT }) => {
|
||||
const url = `${baseUrl}?w=${width}&q=${quality}${
|
||||
IMAGE_OPTIMIZATION.WEBP_SUPPORT ? '&f=webp' : ''
|
||||
}`;
|
||||
return `${url} ${width}w`;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
// Create optimized avatar URL
|
||||
static getOptimizedAvatarUrl(
|
||||
baseUrl: string,
|
||||
size: keyof typeof IMAGE_OPTIMIZATION.SIZES = 'AVATAR_MEDIUM'
|
||||
): string {
|
||||
const dimensions = IMAGE_OPTIMIZATION.SIZES[size];
|
||||
const quality = IMAGE_OPTIMIZATION.QUALITY.AVATAR;
|
||||
|
||||
return `${baseUrl}?w=${dimensions.width}&h=${dimensions.height}&q=${quality}${
|
||||
IMAGE_OPTIMIZATION.WEBP_SUPPORT ? '&f=webp' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
// Create optimized icon URL
|
||||
static getOptimizedIconUrl(
|
||||
baseUrl: string,
|
||||
size: keyof typeof IMAGE_OPTIMIZATION.SIZES = 'ICON_MEDIUM'
|
||||
): string {
|
||||
const dimensions = IMAGE_OPTIMIZATION.SIZES[size];
|
||||
|
||||
return `${baseUrl}?w=${dimensions.width}&h=${dimensions.height}&q=100${
|
||||
IMAGE_OPTIMIZATION.WEBP_SUPPORT ? '&f=webp' : ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Asset caching utilities
|
||||
export class AssetCache {
|
||||
private static cache = new Map<string, { data: any; timestamp: number; duration: number }>();
|
||||
|
||||
// Set item in cache with TTL
|
||||
static set(key: string, data: any, duration: number = CACHE_STRATEGIES.DURATIONS.DYNAMIC_CONTENT): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
duration: duration * 1000, // Convert to milliseconds
|
||||
});
|
||||
|
||||
// Clean up expired items periodically
|
||||
if (this.cache.size % 50 === 0) {
|
||||
this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Get item from cache
|
||||
static get<T>(key: string): T | null {
|
||||
const item = this.cache.get(key);
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() - item.timestamp > item.duration) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.data;
|
||||
}
|
||||
|
||||
// Remove expired items
|
||||
static cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const [key, item] of this.cache.entries()) {
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all cache
|
||||
static clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
// Get cache size and statistics
|
||||
static getStats(): { size: number; totalItems: number; hitRate: number } {
|
||||
return {
|
||||
size: this.cache.size,
|
||||
totalItems: this.cache.size,
|
||||
hitRate: 0, // Could be implemented with counters
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy loading utilities
|
||||
export class LazyLoader {
|
||||
private static observer: IntersectionObserver | null = null;
|
||||
private static loadedImages = new Set<string>();
|
||||
|
||||
// Initialize intersection observer
|
||||
private static getObserver(): IntersectionObserver {
|
||||
if (!this.observer) {
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target as HTMLImageElement;
|
||||
this.loadImage(img);
|
||||
this.observer?.unobserve(img);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '50px', // Start loading 50px before entering viewport
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.observer;
|
||||
}
|
||||
|
||||
// Setup lazy loading for an image
|
||||
static setupLazyLoading(img: HTMLImageElement, src: string): void {
|
||||
if (this.loadedImages.has(src)) {
|
||||
img.src = src;
|
||||
return;
|
||||
}
|
||||
|
||||
img.dataset.src = src;
|
||||
img.classList.add('lazy-loading');
|
||||
this.getObserver().observe(img);
|
||||
}
|
||||
|
||||
// Load image and handle caching
|
||||
private static loadImage(img: HTMLImageElement): void {
|
||||
const src = img.dataset.src;
|
||||
if (!src) return;
|
||||
|
||||
// Check cache first
|
||||
const cachedBlob = AssetCache.get<string>(`image_${src}`);
|
||||
if (cachedBlob) {
|
||||
img.src = cachedBlob;
|
||||
img.classList.remove('lazy-loading');
|
||||
img.classList.add('lazy-loaded');
|
||||
this.loadedImages.add(src);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load and cache image
|
||||
const newImg = new Image();
|
||||
newImg.onload = () => {
|
||||
img.src = src;
|
||||
img.classList.remove('lazy-loading');
|
||||
img.classList.add('lazy-loaded');
|
||||
this.loadedImages.add(src);
|
||||
|
||||
// Cache for future use
|
||||
AssetCache.set(`image_${src}`, src, CACHE_STRATEGIES.DURATIONS.IMAGES);
|
||||
};
|
||||
newImg.onerror = () => {
|
||||
img.classList.remove('lazy-loading');
|
||||
img.classList.add('lazy-error');
|
||||
};
|
||||
newImg.src = src;
|
||||
}
|
||||
|
||||
// Preload critical images
|
||||
static preloadCriticalImages(urls: string[]): Promise<void[]> {
|
||||
return Promise.all(
|
||||
urls.map((url) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
this.loadedImages.add(url);
|
||||
AssetCache.set(`image_${url}`, url, CACHE_STRATEGIES.DURATIONS.IMAGES);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Progressive loading utilities
|
||||
export class ProgressiveLoader {
|
||||
// Create progressive JPEG-like loading effect
|
||||
static createProgressiveImage(
|
||||
container: HTMLElement,
|
||||
lowQualitySrc: string,
|
||||
highQualitySrc: string
|
||||
): void {
|
||||
const lowQualityImg = document.createElement('img');
|
||||
const highQualityImg = document.createElement('img');
|
||||
|
||||
// Style for smooth transition
|
||||
const baseStyle = {
|
||||
position: 'absolute' as const,
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover' as const,
|
||||
};
|
||||
|
||||
Object.assign(lowQualityImg.style, baseStyle, {
|
||||
filter: 'blur(2px)',
|
||||
transition: 'opacity 0.3s ease',
|
||||
});
|
||||
|
||||
Object.assign(highQualityImg.style, baseStyle, {
|
||||
opacity: '0',
|
||||
transition: 'opacity 0.3s ease',
|
||||
});
|
||||
|
||||
// Load low quality first
|
||||
lowQualityImg.src = lowQualitySrc;
|
||||
container.appendChild(lowQualityImg);
|
||||
container.appendChild(highQualityImg);
|
||||
|
||||
// Load high quality and fade in
|
||||
highQualityImg.onload = () => {
|
||||
highQualityImg.style.opacity = '1';
|
||||
setTimeout(() => {
|
||||
lowQualityImg.remove();
|
||||
}, 300);
|
||||
};
|
||||
highQualityImg.src = highQualitySrc;
|
||||
}
|
||||
}
|
||||
|
||||
// Asset preloading strategies
|
||||
export class AssetPreloader {
|
||||
private static preloadedAssets = new Set<string>();
|
||||
|
||||
// Preload assets based on priority
|
||||
static preloadAssets(assets: Array<{ url: string; priority: 'high' | 'medium' | 'low' }>): void {
|
||||
// Sort by priority
|
||||
assets.sort((a, b) => {
|
||||
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
||||
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
||||
});
|
||||
|
||||
// Preload high priority assets immediately
|
||||
const highPriorityAssets = assets.filter(asset => asset.priority === 'high');
|
||||
this.preloadImmediately(highPriorityAssets.map(a => a.url));
|
||||
|
||||
// Preload medium priority assets after a short delay
|
||||
setTimeout(() => {
|
||||
const mediumPriorityAssets = assets.filter(asset => asset.priority === 'medium');
|
||||
this.preloadWithIdleCallback(mediumPriorityAssets.map(a => a.url));
|
||||
}, 100);
|
||||
|
||||
// Preload low priority assets when browser is idle
|
||||
setTimeout(() => {
|
||||
const lowPriorityAssets = assets.filter(asset => asset.priority === 'low');
|
||||
this.preloadWithIdleCallback(lowPriorityAssets.map(a => a.url));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Immediate preloading for critical assets
|
||||
private static preloadImmediately(urls: string[]): void {
|
||||
urls.forEach(url => {
|
||||
if (this.preloadedAssets.has(url)) return;
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.href = url;
|
||||
|
||||
// Determine asset type
|
||||
if (url.match(/\.(jpg|jpeg|png|webp|gif)$/i)) {
|
||||
link.as = 'image';
|
||||
} else if (url.match(/\.(woff|woff2|ttf|otf)$/i)) {
|
||||
link.as = 'font';
|
||||
link.crossOrigin = 'anonymous';
|
||||
} else if (url.match(/\.(css)$/i)) {
|
||||
link.as = 'style';
|
||||
} else if (url.match(/\.(js)$/i)) {
|
||||
link.as = 'script';
|
||||
}
|
||||
|
||||
document.head.appendChild(link);
|
||||
this.preloadedAssets.add(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Preload with idle callback for non-critical assets
|
||||
private static preloadWithIdleCallback(urls: string[]): void {
|
||||
const preloadBatch = () => {
|
||||
urls.forEach(url => {
|
||||
if (this.preloadedAssets.has(url)) return;
|
||||
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
this.preloadedAssets.add(url);
|
||||
});
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
(window as any).requestIdleCallback(preloadBatch, { timeout: 2000 });
|
||||
} else {
|
||||
setTimeout(preloadBatch, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSS for optimized image loading
|
||||
export const imageOptimizationStyles = `
|
||||
/* Lazy loading states */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lazy-error::after {
|
||||
content: '⚠️';
|
||||
font-size: 24px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Shimmer animation for loading */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Fade in animation */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Progressive image container */
|
||||
.progressive-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Responsive image utilities */
|
||||
.responsive-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Avatar optimization */
|
||||
.optimized-avatar {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Icon optimization */
|
||||
.optimized-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Preload critical images */
|
||||
.critical-image {
|
||||
object-fit: cover;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
`;
|
||||
|
||||
// Utility functions
|
||||
export const AssetUtils = {
|
||||
// Get file size from data URL
|
||||
getDataUrlSize: (dataUrl: string): number => {
|
||||
const base64String = dataUrl.split(',')[1];
|
||||
return Math.round((base64String.length * 3) / 4);
|
||||
},
|
||||
|
||||
// Convert file size to human readable format
|
||||
formatFileSize: (bytes: number): string => {
|
||||
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];
|
||||
},
|
||||
|
||||
// Generate low quality placeholder
|
||||
generatePlaceholder: (width: number, height: number, color: string = '#e5e7eb'): string => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
return canvas.toDataURL('image/png');
|
||||
},
|
||||
|
||||
// Check if image is already cached
|
||||
isImageCached: (url: string): boolean => {
|
||||
return AssetCache.get(`image_${url}`) !== null;
|
||||
},
|
||||
|
||||
// Prefetch critical resources
|
||||
prefetchResources: (urls: string[]): void => {
|
||||
urls.forEach(url => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'prefetch';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
},
|
||||
};
|
||||
598
worklenz-frontend/src/utils/css-optimizations.ts
Normal file
598
worklenz-frontend/src/utils/css-optimizations.ts
Normal file
@@ -0,0 +1,598 @@
|
||||
// CSS optimization utilities for improved performance and reduced layout shifts
|
||||
|
||||
// Critical CSS constants
|
||||
export const CSS_OPTIMIZATION = {
|
||||
// Performance thresholds
|
||||
THRESHOLDS: {
|
||||
CRITICAL_CSS_SIZE: 14000, // 14KB critical CSS limit
|
||||
INLINE_CSS_LIMIT: 4000, // 4KB inline CSS limit
|
||||
UNUSED_CSS_THRESHOLD: 80, // Remove CSS with <80% usage
|
||||
},
|
||||
|
||||
// Layout shift prevention
|
||||
LAYOUT_PREVENTION: {
|
||||
// Common aspect ratios for media
|
||||
ASPECT_RATIOS: {
|
||||
SQUARE: '1:1',
|
||||
LANDSCAPE: '16:9',
|
||||
PORTRAIT: '9:16',
|
||||
CARD: '4:3',
|
||||
WIDE: '21:9',
|
||||
},
|
||||
|
||||
// Standard sizes for common elements
|
||||
PLACEHOLDER_SIZES: {
|
||||
AVATAR: { width: 40, height: 40 },
|
||||
BUTTON: { width: 120, height: 36 },
|
||||
INPUT: { width: 200, height: 40 },
|
||||
CARD: { width: 300, height: 200 },
|
||||
THUMBNAIL: { width: 64, height: 64 },
|
||||
},
|
||||
},
|
||||
|
||||
// CSS optimization strategies
|
||||
STRATEGIES: {
|
||||
CRITICAL_ABOVE_FOLD: ['layout', 'typography', 'colors', 'spacing'],
|
||||
DEFER_BELOW_FOLD: ['animations', 'hover-effects', 'non-critical-components'],
|
||||
INLINE_CRITICAL: ['reset', 'grid', 'typography', 'critical-components'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
// CSS performance monitoring
|
||||
export class CSSPerformanceMonitor {
|
||||
private static metrics = {
|
||||
layoutShifts: 0,
|
||||
renderBlockingCSS: 0,
|
||||
unusedCSS: 0,
|
||||
criticalCSSSize: 0,
|
||||
};
|
||||
|
||||
// Monitor Cumulative Layout Shift (CLS)
|
||||
static monitorLayoutShifts(): () => void {
|
||||
if (!('PerformanceObserver' in window)) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.entryType === 'layout-shift' && !(entry as any).hadRecentInput) {
|
||||
this.metrics.layoutShifts += (entry as any).value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe({ type: 'layout-shift', buffered: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
// Monitor render-blocking resources
|
||||
static monitorRenderBlocking(): void {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.name.endsWith('.css') && (entry as any).renderBlockingStatus === 'blocking') {
|
||||
this.metrics.renderBlockingCSS++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe({ type: 'resource', buffered: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Get current metrics
|
||||
static getMetrics() {
|
||||
return { ...this.metrics };
|
||||
}
|
||||
|
||||
// Reset metrics
|
||||
static reset(): void {
|
||||
this.metrics = {
|
||||
layoutShifts: 0,
|
||||
renderBlockingCSS: 0,
|
||||
unusedCSS: 0,
|
||||
criticalCSSSize: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Layout shift prevention utilities
|
||||
export class LayoutStabilizer {
|
||||
// Create placeholder with known dimensions
|
||||
static createPlaceholder(
|
||||
element: HTMLElement,
|
||||
dimensions: { width?: number; height?: number; aspectRatio?: string }
|
||||
): void {
|
||||
const { width, height, aspectRatio } = dimensions;
|
||||
|
||||
if (aspectRatio) {
|
||||
element.style.aspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
if (width) {
|
||||
element.style.width = `${width}px`;
|
||||
}
|
||||
|
||||
if (height) {
|
||||
element.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
// Prevent layout shifts during loading
|
||||
element.style.minHeight = height ? `${height}px` : '1px';
|
||||
element.style.containIntrinsicSize = width && height ? `${width}px ${height}px` : 'auto';
|
||||
}
|
||||
|
||||
// Reserve space for dynamic content
|
||||
static reserveSpace(
|
||||
container: HTMLElement,
|
||||
estimatedHeight: number,
|
||||
adjustOnLoad: boolean = true
|
||||
): () => void {
|
||||
const originalHeight = container.style.height;
|
||||
container.style.minHeight = `${estimatedHeight}px`;
|
||||
|
||||
if (adjustOnLoad) {
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (container.scrollHeight > estimatedHeight) {
|
||||
container.style.minHeight = 'auto';
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(container);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
container.style.height = originalHeight;
|
||||
container.style.minHeight = 'auto';
|
||||
};
|
||||
}
|
||||
|
||||
// Preload fonts to prevent text layout shifts
|
||||
static preloadFonts(fontFaces: Array<{ family: string; weight?: string; style?: string }>): void {
|
||||
fontFaces.forEach(({ family, weight = '400', style = 'normal' }) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.as = 'font';
|
||||
link.type = 'font/woff2';
|
||||
link.crossOrigin = 'anonymous';
|
||||
link.href = `/fonts/${family}-${weight}-${style}.woff2`;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply size-based CSS containment
|
||||
static applyContainment(element: HTMLElement, type: 'size' | 'layout' | 'style' | 'paint'): void {
|
||||
element.style.contain = type;
|
||||
}
|
||||
}
|
||||
|
||||
// Critical CSS management
|
||||
export class CriticalCSSManager {
|
||||
private static criticalCSS = new Set<string>();
|
||||
private static deferredCSS = new Set<string>();
|
||||
|
||||
// Identify critical CSS selectors
|
||||
static identifyCriticalCSS(): string[] {
|
||||
const criticalSelectors: string[] = [];
|
||||
|
||||
// Get above-the-fold elements
|
||||
const viewportHeight = window.innerHeight;
|
||||
const aboveFoldElements = Array.from(document.querySelectorAll('*')).filter(
|
||||
(el) => el.getBoundingClientRect().top < viewportHeight
|
||||
);
|
||||
|
||||
// Extract CSS rules for above-the-fold elements
|
||||
aboveFoldElements.forEach((element) => {
|
||||
const computedStyle = window.getComputedStyle(element);
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
const className = element.className;
|
||||
const id = element.id;
|
||||
|
||||
// Add tag selectors
|
||||
criticalSelectors.push(tagName);
|
||||
|
||||
// Add class selectors
|
||||
if (className) {
|
||||
className.split(' ').forEach((cls) => {
|
||||
criticalSelectors.push(`.${cls}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Add ID selectors
|
||||
if (id) {
|
||||
criticalSelectors.push(`#${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(new Set(criticalSelectors));
|
||||
}
|
||||
|
||||
// Extract critical CSS
|
||||
static async extractCriticalCSS(html: string, css: string): Promise<string> {
|
||||
// This is a simplified version - in production, use tools like critical or penthouse
|
||||
const criticalSelectors = this.identifyCriticalCSS();
|
||||
const criticalRules: string[] = [];
|
||||
|
||||
// Parse CSS and extract matching rules
|
||||
const cssRules = css.split('}').map(rule => rule.trim() + '}');
|
||||
|
||||
cssRules.forEach((rule) => {
|
||||
for (const selector of criticalSelectors) {
|
||||
if (rule.includes(selector)) {
|
||||
criticalRules.push(rule);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return criticalRules.join('\n');
|
||||
}
|
||||
|
||||
// Inline critical CSS
|
||||
static inlineCriticalCSS(css: string): void {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
style.setAttribute('data-critical', 'true');
|
||||
document.head.insertBefore(style, document.head.firstChild);
|
||||
}
|
||||
|
||||
// Load non-critical CSS asynchronously
|
||||
static loadNonCriticalCSS(href: string, media: string = 'all'): void {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.as = 'style';
|
||||
link.href = href;
|
||||
link.media = 'print'; // Load as print to avoid blocking
|
||||
link.onload = () => {
|
||||
link.media = media; // Switch to target media once loaded
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
// CSS optimization utilities
|
||||
export class CSSOptimizer {
|
||||
// Remove unused CSS selectors
|
||||
static removeUnusedCSS(css: string): string {
|
||||
const usedSelectors = new Set<string>();
|
||||
|
||||
// Get all elements and their classes/IDs
|
||||
document.querySelectorAll('*').forEach((element) => {
|
||||
usedSelectors.add(element.tagName.toLowerCase());
|
||||
|
||||
if (element.className) {
|
||||
element.className.split(' ').forEach((cls) => {
|
||||
usedSelectors.add(`.${cls}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (element.id) {
|
||||
usedSelectors.add(`#${element.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter CSS rules
|
||||
const cssRules = css.split('}');
|
||||
const optimizedRules = cssRules.filter((rule) => {
|
||||
const selectorPart = rule.split('{')[0];
|
||||
if (!selectorPart) return false;
|
||||
const selector = selectorPart.trim();
|
||||
if (!selector) return false;
|
||||
|
||||
// Check if selector is used
|
||||
return Array.from(usedSelectors).some((used) =>
|
||||
selector.includes(used)
|
||||
);
|
||||
});
|
||||
|
||||
return optimizedRules.join('}');
|
||||
}
|
||||
|
||||
// Minify CSS
|
||||
static minifyCSS(css: string): string {
|
||||
return css
|
||||
// Remove comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
// Remove unnecessary whitespace
|
||||
.replace(/\s+/g, ' ')
|
||||
// Remove whitespace around selectors and properties
|
||||
.replace(/\s*{\s*/g, '{')
|
||||
.replace(/;\s*/g, ';')
|
||||
.replace(/}\s*/g, '}')
|
||||
// Remove trailing semicolons
|
||||
.replace(/;}/g, '}')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Bundle CSS efficiently
|
||||
static bundleCSS(cssFiles: string[]): Promise<string> {
|
||||
return Promise.all(
|
||||
cssFiles.map(async (file) => {
|
||||
const response = await fetch(file);
|
||||
return response.text();
|
||||
})
|
||||
).then((styles) => {
|
||||
const bundled = styles.join('\n');
|
||||
return this.minifyCSS(bundled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic CSS loading utilities
|
||||
export class DynamicCSSLoader {
|
||||
private static loadedStylesheets = new Set<string>();
|
||||
private static loadingPromises = new Map<string, Promise<void>>();
|
||||
|
||||
// Load CSS on demand
|
||||
static async loadCSS(href: string, options: {
|
||||
media?: string;
|
||||
priority?: 'high' | 'low';
|
||||
critical?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { media = 'all', priority = 'low', critical = false } = options;
|
||||
|
||||
if (this.loadedStylesheets.has(href)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.loadingPromises.has(href)) {
|
||||
return this.loadingPromises.get(href)!;
|
||||
}
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = critical ? 'stylesheet' : 'preload';
|
||||
link.as = critical ? undefined : 'style';
|
||||
link.href = href;
|
||||
link.media = media;
|
||||
|
||||
if (priority === 'high') {
|
||||
link.setAttribute('importance', 'high');
|
||||
}
|
||||
|
||||
link.onload = () => {
|
||||
if (!critical) {
|
||||
link.rel = 'stylesheet';
|
||||
}
|
||||
this.loadedStylesheets.add(href);
|
||||
this.loadingPromises.delete(href);
|
||||
resolve();
|
||||
};
|
||||
|
||||
link.onerror = () => {
|
||||
this.loadingPromises.delete(href);
|
||||
reject(new Error(`Failed to load CSS: ${href}`));
|
||||
};
|
||||
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
|
||||
this.loadingPromises.set(href, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Load CSS based on component visibility
|
||||
static loadCSSOnIntersection(
|
||||
element: HTMLElement,
|
||||
cssHref: string,
|
||||
options: { rootMargin?: string; threshold?: number } = {}
|
||||
): () => void {
|
||||
const { rootMargin = '100px', threshold = 0.1 } = options;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadCSS(cssHref);
|
||||
observer.unobserve(element);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin, threshold }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
// Load CSS based on user interaction
|
||||
static loadCSSOnInteraction(
|
||||
element: HTMLElement,
|
||||
cssHref: string,
|
||||
events: string[] = ['mouseenter', 'touchstart']
|
||||
): () => void {
|
||||
const loadCSS = () => {
|
||||
this.loadCSS(cssHref);
|
||||
cleanup();
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
events.forEach((event) => {
|
||||
element.removeEventListener(event, loadCSS);
|
||||
});
|
||||
};
|
||||
|
||||
events.forEach((event) => {
|
||||
element.addEventListener(event, loadCSS, { once: true, passive: true });
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
// CSS performance optimization styles
|
||||
export const cssPerformanceStyles = `
|
||||
/* Layout shift prevention */
|
||||
.prevent-layout-shift {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* Efficient animations */
|
||||
.gpu-accelerated {
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.efficient-transition {
|
||||
transition: transform 0.2s ease-out, opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Critical loading states */
|
||||
.critical-loading {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Font loading optimization */
|
||||
.font-loading {
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Container queries for responsive design */
|
||||
.container-responsive {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (min-width: 300px) {
|
||||
.container-responsive .content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS containment for performance */
|
||||
.layout-contained {
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.paint-contained {
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
.size-contained {
|
||||
contain: size;
|
||||
}
|
||||
|
||||
.style-contained {
|
||||
contain: style;
|
||||
}
|
||||
|
||||
/* Optimized scrolling */
|
||||
.smooth-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Prevent repaints during animations */
|
||||
.animation-optimized {
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
/* Critical path optimizations */
|
||||
.above-fold {
|
||||
priority: 1;
|
||||
}
|
||||
|
||||
.below-fold {
|
||||
priority: 0;
|
||||
}
|
||||
|
||||
/* Resource hints via CSS */
|
||||
.preload-critical::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-image: url('/critical-image.webp');
|
||||
}
|
||||
`;
|
||||
|
||||
// Utility functions for CSS optimization
|
||||
export const CSSUtils = {
|
||||
// Calculate CSS specificity
|
||||
calculateSpecificity: (selector: string): number => {
|
||||
const idCount = (selector.match(/#/g) || []).length;
|
||||
const classCount = (selector.match(/\./g) || []).length;
|
||||
const elementCount = (selector.match(/[a-zA-Z]/g) || []).length;
|
||||
|
||||
return idCount * 100 + classCount * 10 + elementCount;
|
||||
},
|
||||
|
||||
// Check if CSS property is supported
|
||||
isPropertySupported: (property: string, value: string): boolean => {
|
||||
const element = document.createElement('div');
|
||||
element.style.setProperty(property, value);
|
||||
return element.style.getPropertyValue(property) === value;
|
||||
},
|
||||
|
||||
// Get critical viewport CSS
|
||||
getCriticalViewportCSS: (): { width: number; height: number; ratio: number } => {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
ratio: window.innerWidth / window.innerHeight,
|
||||
};
|
||||
},
|
||||
|
||||
// Optimize CSS custom properties
|
||||
optimizeCustomProperties: (css: string): string => {
|
||||
// Group related custom properties
|
||||
const optimized = css.replace(
|
||||
/:root\s*{([^}]*)}/g,
|
||||
(match, properties) => {
|
||||
const sorted = properties
|
||||
.split(';')
|
||||
.filter((prop: string) => prop.trim())
|
||||
.sort()
|
||||
.join(';');
|
||||
return `:root{${sorted}}`;
|
||||
}
|
||||
);
|
||||
|
||||
return optimized;
|
||||
},
|
||||
|
||||
// Generate responsive CSS
|
||||
generateResponsiveCSS: (
|
||||
selector: string,
|
||||
properties: Record<string, string>,
|
||||
breakpoints: Record<string, string>
|
||||
): string => {
|
||||
let css = `${selector} { ${Object.entries(properties).map(([prop, value]) => `${prop}: ${value}`).join('; ')} }`;
|
||||
|
||||
Object.entries(breakpoints).forEach(([breakpoint, mediaQuery]) => {
|
||||
css += `\n@media ${mediaQuery} { ${selector} { /* responsive styles */ } }`;
|
||||
});
|
||||
|
||||
return css;
|
||||
},
|
||||
|
||||
// Check for CSS performance issues
|
||||
checkPerformanceIssues: (css: string): string[] => {
|
||||
const issues: string[] = [];
|
||||
|
||||
// Check for expensive selectors
|
||||
if (css.includes('*')) {
|
||||
issues.push('Universal selector (*) detected - may impact performance');
|
||||
}
|
||||
|
||||
// Check for inefficient descendant selectors
|
||||
const deepSelectors = css.match(/(\w+\s+){4,}/g);
|
||||
if (deepSelectors) {
|
||||
issues.push('Deep descendant selectors detected - consider using more specific classes');
|
||||
}
|
||||
|
||||
// Check for !important overuse
|
||||
const importantCount = (css.match(/!important/g) || []).length;
|
||||
if (importantCount > 10) {
|
||||
issues.push('Excessive use of !important detected');
|
||||
}
|
||||
|
||||
return issues;
|
||||
},
|
||||
};
|
||||
680
worklenz-frontend/src/utils/enhanced-performance-monitoring.ts
Normal file
680
worklenz-frontend/src/utils/enhanced-performance-monitoring.ts
Normal file
@@ -0,0 +1,680 @@
|
||||
// Enhanced performance monitoring for Worklenz application
|
||||
|
||||
// Performance monitoring constants
|
||||
export const PERFORMANCE_CONFIG = {
|
||||
// Measurement thresholds
|
||||
THRESHOLDS: {
|
||||
FCP: 1800, // First Contentful Paint (ms)
|
||||
LCP: 2500, // Largest Contentful Paint (ms)
|
||||
FID: 100, // First Input Delay (ms)
|
||||
CLS: 0.1, // Cumulative Layout Shift
|
||||
TTFB: 600, // Time to First Byte (ms)
|
||||
INP: 200, // Interaction to Next Paint (ms)
|
||||
},
|
||||
|
||||
// Monitoring intervals
|
||||
INTERVALS: {
|
||||
METRICS_COLLECTION: 5000, // 5 seconds
|
||||
PERFORMANCE_REPORT: 30000, // 30 seconds
|
||||
CLEANUP_THRESHOLD: 300000, // 5 minutes
|
||||
},
|
||||
|
||||
// Buffer sizes
|
||||
BUFFERS: {
|
||||
MAX_ENTRIES: 1000,
|
||||
MAX_RESOURCE_ENTRIES: 500,
|
||||
MAX_NAVIGATION_ENTRIES: 100,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Performance metrics interface
|
||||
export interface PerformanceMetrics {
|
||||
// Core Web Vitals
|
||||
fcp?: number;
|
||||
lcp?: number;
|
||||
fid?: number;
|
||||
cls?: number;
|
||||
ttfb?: number;
|
||||
inp?: number;
|
||||
|
||||
// Custom metrics
|
||||
domContentLoaded?: number;
|
||||
windowLoad?: number;
|
||||
firstByte?: number;
|
||||
|
||||
// Application-specific metrics
|
||||
taskLoadTime?: number;
|
||||
projectSwitchTime?: number;
|
||||
filterApplyTime?: number;
|
||||
bulkActionTime?: number;
|
||||
|
||||
// Memory and performance
|
||||
memoryUsage?: {
|
||||
usedJSHeapSize: number;
|
||||
totalJSHeapSize: number;
|
||||
jsHeapSizeLimit: number;
|
||||
};
|
||||
|
||||
// Timing information
|
||||
timestamp: number;
|
||||
url: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
// Performance monitoring class
|
||||
export class EnhancedPerformanceMonitor {
|
||||
private static instance: EnhancedPerformanceMonitor;
|
||||
private metrics: PerformanceMetrics[] = [];
|
||||
private observers: PerformanceObserver[] = [];
|
||||
private intervalIds: NodeJS.Timeout[] = [];
|
||||
private isMonitoring = false;
|
||||
|
||||
// Singleton pattern
|
||||
static getInstance(): EnhancedPerformanceMonitor {
|
||||
if (!this.instance) {
|
||||
this.instance = new EnhancedPerformanceMonitor();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
// Start comprehensive performance monitoring
|
||||
startMonitoring(): void {
|
||||
if (this.isMonitoring) return;
|
||||
|
||||
this.isMonitoring = true;
|
||||
this.setupObservers();
|
||||
this.collectInitialMetrics();
|
||||
this.startPeriodicCollection();
|
||||
|
||||
console.log('🚀 Enhanced performance monitoring started');
|
||||
}
|
||||
|
||||
// Stop monitoring and cleanup
|
||||
stopMonitoring(): void {
|
||||
if (!this.isMonitoring) return;
|
||||
|
||||
this.isMonitoring = false;
|
||||
this.cleanupObservers();
|
||||
this.clearIntervals();
|
||||
|
||||
console.log('🛑 Enhanced performance monitoring stopped');
|
||||
}
|
||||
|
||||
// Setup performance observers
|
||||
private setupObservers(): void {
|
||||
if (!('PerformanceObserver' in window)) return;
|
||||
|
||||
// Core Web Vitals observer
|
||||
try {
|
||||
const vitalsObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
this.processVitalMetric(entry);
|
||||
}
|
||||
});
|
||||
|
||||
vitalsObserver.observe({
|
||||
type: 'largest-contentful-paint',
|
||||
buffered: true
|
||||
});
|
||||
|
||||
vitalsObserver.observe({
|
||||
type: 'first-input',
|
||||
buffered: true
|
||||
});
|
||||
|
||||
vitalsObserver.observe({
|
||||
type: 'layout-shift',
|
||||
buffered: true
|
||||
});
|
||||
|
||||
this.observers.push(vitalsObserver);
|
||||
} catch (error) {
|
||||
console.warn('Failed to setup vitals observer:', error);
|
||||
}
|
||||
|
||||
// Navigation timing observer
|
||||
try {
|
||||
const navigationObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
this.processNavigationMetric(entry as PerformanceNavigationTiming);
|
||||
}
|
||||
});
|
||||
|
||||
navigationObserver.observe({
|
||||
type: 'navigation',
|
||||
buffered: true
|
||||
});
|
||||
|
||||
this.observers.push(navigationObserver);
|
||||
} catch (error) {
|
||||
console.warn('Failed to setup navigation observer:', error);
|
||||
}
|
||||
|
||||
// Resource timing observer
|
||||
try {
|
||||
const resourceObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
this.processResourceMetric(entry as PerformanceResourceTiming);
|
||||
}
|
||||
});
|
||||
|
||||
resourceObserver.observe({
|
||||
type: 'resource',
|
||||
buffered: true
|
||||
});
|
||||
|
||||
this.observers.push(resourceObserver);
|
||||
} catch (error) {
|
||||
console.warn('Failed to setup resource observer:', error);
|
||||
}
|
||||
|
||||
// Measure observer
|
||||
try {
|
||||
const measureObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
this.processCustomMeasure(entry as PerformanceMeasure);
|
||||
}
|
||||
});
|
||||
|
||||
measureObserver.observe({
|
||||
type: 'measure',
|
||||
buffered: true
|
||||
});
|
||||
|
||||
this.observers.push(measureObserver);
|
||||
} catch (error) {
|
||||
console.warn('Failed to setup measure observer:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Process Core Web Vitals metrics
|
||||
private processVitalMetric(entry: PerformanceEntry): void {
|
||||
const metric: Partial<PerformanceMetrics> = {
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
};
|
||||
|
||||
switch (entry.entryType) {
|
||||
case 'largest-contentful-paint':
|
||||
metric.lcp = entry.startTime;
|
||||
break;
|
||||
case 'first-input':
|
||||
metric.fid = (entry as any).processingStart - entry.startTime;
|
||||
break;
|
||||
case 'layout-shift':
|
||||
if (!(entry as any).hadRecentInput) {
|
||||
metric.cls = (metric.cls || 0) + (entry as any).value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.addMetric(metric as PerformanceMetrics);
|
||||
}
|
||||
|
||||
// Process navigation timing metrics
|
||||
private processNavigationMetric(entry: PerformanceNavigationTiming): void {
|
||||
const metric: PerformanceMetrics = {
|
||||
fcp: this.getFCP(),
|
||||
ttfb: entry.responseStart - entry.requestStart,
|
||||
domContentLoaded: entry.domContentLoadedEventEnd - entry.startTime,
|
||||
windowLoad: entry.loadEventEnd - entry.startTime,
|
||||
firstByte: entry.responseStart - entry.startTime,
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
};
|
||||
|
||||
this.addMetric(metric);
|
||||
}
|
||||
|
||||
// Process resource timing metrics
|
||||
private processResourceMetric(entry: PerformanceResourceTiming): void {
|
||||
// Track slow resources
|
||||
const duration = entry.responseEnd - entry.requestStart;
|
||||
|
||||
if (duration > 1000) { // Resources taking more than 1 second
|
||||
console.warn(`Slow resource detected: ${entry.name} (${duration.toFixed(2)}ms)`);
|
||||
}
|
||||
|
||||
// Track render-blocking resources (check if property exists)
|
||||
if ((entry as any).renderBlockingStatus === 'blocking') {
|
||||
console.warn(`Render-blocking resource: ${entry.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Process custom performance measures
|
||||
private processCustomMeasure(entry: PerformanceMeasure): void {
|
||||
const metric: Partial<PerformanceMetrics> = {
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
};
|
||||
|
||||
// Map custom measures to metrics
|
||||
switch (entry.name) {
|
||||
case 'task-load-time':
|
||||
metric.taskLoadTime = entry.duration;
|
||||
break;
|
||||
case 'project-switch-time':
|
||||
metric.projectSwitchTime = entry.duration;
|
||||
break;
|
||||
case 'filter-apply-time':
|
||||
metric.filterApplyTime = entry.duration;
|
||||
break;
|
||||
case 'bulk-action-time':
|
||||
metric.bulkActionTime = entry.duration;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Object.keys(metric).length > 3) {
|
||||
this.addMetric(metric as PerformanceMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
// Get First Contentful Paint
|
||||
private getFCP(): number | undefined {
|
||||
const fcpEntry = performance.getEntriesByType('paint')
|
||||
.find(entry => entry.name === 'first-contentful-paint');
|
||||
return fcpEntry?.startTime;
|
||||
}
|
||||
|
||||
// Collect initial metrics
|
||||
private collectInitialMetrics(): void {
|
||||
const metric: PerformanceMetrics = {
|
||||
fcp: this.getFCP(),
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
};
|
||||
|
||||
// Add memory information if available
|
||||
if ('memory' in performance) {
|
||||
metric.memoryUsage = {
|
||||
usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
|
||||
totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: (performance as any).memory.jsHeapSizeLimit,
|
||||
};
|
||||
}
|
||||
|
||||
this.addMetric(metric);
|
||||
}
|
||||
|
||||
// Start periodic metrics collection
|
||||
private startPeriodicCollection(): void {
|
||||
// Collect metrics every 5 seconds
|
||||
const metricsInterval = setInterval(() => {
|
||||
this.collectPeriodicMetrics();
|
||||
}, PERFORMANCE_CONFIG.INTERVALS.METRICS_COLLECTION);
|
||||
|
||||
// Generate performance report every 30 seconds
|
||||
const reportInterval = setInterval(() => {
|
||||
this.generatePerformanceReport();
|
||||
}, PERFORMANCE_CONFIG.INTERVALS.PERFORMANCE_REPORT);
|
||||
|
||||
// Cleanup old metrics every 5 minutes
|
||||
const cleanupInterval = setInterval(() => {
|
||||
this.cleanupOldMetrics();
|
||||
}, PERFORMANCE_CONFIG.INTERVALS.CLEANUP_THRESHOLD);
|
||||
|
||||
this.intervalIds.push(metricsInterval, reportInterval, cleanupInterval);
|
||||
}
|
||||
|
||||
// Collect periodic metrics
|
||||
private collectPeriodicMetrics(): void {
|
||||
const metric: PerformanceMetrics = {
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
};
|
||||
|
||||
// Add memory information if available
|
||||
if ('memory' in performance) {
|
||||
metric.memoryUsage = {
|
||||
usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
|
||||
totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: (performance as any).memory.jsHeapSizeLimit,
|
||||
};
|
||||
}
|
||||
|
||||
this.addMetric(metric);
|
||||
}
|
||||
|
||||
// Add metric to collection
|
||||
private addMetric(metric: PerformanceMetrics): void {
|
||||
this.metrics.push(metric);
|
||||
|
||||
// Limit buffer size
|
||||
if (this.metrics.length > PERFORMANCE_CONFIG.BUFFERS.MAX_ENTRIES) {
|
||||
this.metrics = this.metrics.slice(-PERFORMANCE_CONFIG.BUFFERS.MAX_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate performance report
|
||||
private generatePerformanceReport(): void {
|
||||
if (this.metrics.length === 0) return;
|
||||
|
||||
const recent = this.metrics.slice(-10); // Last 10 metrics
|
||||
const report = this.analyzeMetrics(recent);
|
||||
|
||||
console.log('📊 Performance Report:', report);
|
||||
|
||||
// Check for performance issues
|
||||
this.checkPerformanceIssues(report);
|
||||
}
|
||||
|
||||
// Analyze metrics and generate insights
|
||||
private analyzeMetrics(metrics: PerformanceMetrics[]): any {
|
||||
const validMetrics = metrics.filter(m => m);
|
||||
|
||||
if (validMetrics.length === 0) return {};
|
||||
|
||||
const report: any = {
|
||||
timestamp: Date.now(),
|
||||
sampleSize: validMetrics.length,
|
||||
};
|
||||
|
||||
// Analyze each metric
|
||||
['fcp', 'lcp', 'fid', 'cls', 'ttfb', 'taskLoadTime', 'projectSwitchTime'].forEach(metric => {
|
||||
const values = validMetrics
|
||||
.map(m => (m as any)[metric])
|
||||
.filter(v => v !== undefined);
|
||||
|
||||
if (values.length > 0) {
|
||||
report[metric] = {
|
||||
avg: values.reduce((a, b) => a + b, 0) / values.length,
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values),
|
||||
latest: values[values.length - 1],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Memory analysis
|
||||
const memoryMetrics = validMetrics
|
||||
.map(m => m.memoryUsage)
|
||||
.filter(m => m !== undefined);
|
||||
|
||||
if (memoryMetrics.length > 0) {
|
||||
const latest = memoryMetrics[memoryMetrics.length - 1];
|
||||
report.memory = {
|
||||
usedMB: (latest.usedJSHeapSize / 1024 / 1024).toFixed(2),
|
||||
totalMB: (latest.totalJSHeapSize / 1024 / 1024).toFixed(2),
|
||||
usage: ((latest.usedJSHeapSize / latest.totalJSHeapSize) * 100).toFixed(2) + '%',
|
||||
};
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// Check for performance issues
|
||||
private checkPerformanceIssues(report: any): void {
|
||||
const issues: string[] = [];
|
||||
|
||||
// Check Core Web Vitals
|
||||
if (report.fcp?.latest > PERFORMANCE_CONFIG.THRESHOLDS.FCP) {
|
||||
issues.push(`FCP is slow: ${report.fcp.latest.toFixed(2)}ms (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.FCP}ms)`);
|
||||
}
|
||||
|
||||
if (report.lcp?.latest > PERFORMANCE_CONFIG.THRESHOLDS.LCP) {
|
||||
issues.push(`LCP is slow: ${report.lcp.latest.toFixed(2)}ms (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.LCP}ms)`);
|
||||
}
|
||||
|
||||
if (report.fid?.latest > PERFORMANCE_CONFIG.THRESHOLDS.FID) {
|
||||
issues.push(`FID is high: ${report.fid.latest.toFixed(2)}ms (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.FID}ms)`);
|
||||
}
|
||||
|
||||
if (report.cls?.latest > PERFORMANCE_CONFIG.THRESHOLDS.CLS) {
|
||||
issues.push(`CLS is high: ${report.cls.latest.toFixed(3)} (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.CLS})`);
|
||||
}
|
||||
|
||||
// Check application-specific metrics
|
||||
if (report.taskLoadTime?.latest > 1000) {
|
||||
issues.push(`Task loading is slow: ${report.taskLoadTime.latest.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
if (report.projectSwitchTime?.latest > 500) {
|
||||
issues.push(`Project switching is slow: ${report.projectSwitchTime.latest.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Check memory usage
|
||||
if (report.memory && parseFloat(report.memory.usage) > 80) {
|
||||
issues.push(`High memory usage: ${report.memory.usage}`);
|
||||
}
|
||||
|
||||
// Log issues
|
||||
if (issues.length > 0) {
|
||||
console.warn('⚠️ Performance Issues Detected:');
|
||||
issues.forEach(issue => console.warn(` - ${issue}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup old metrics
|
||||
private cleanupOldMetrics(): void {
|
||||
const fiveMinutesAgo = Date.now() - PERFORMANCE_CONFIG.INTERVALS.CLEANUP_THRESHOLD;
|
||||
this.metrics = this.metrics.filter(metric => metric.timestamp > fiveMinutesAgo);
|
||||
}
|
||||
|
||||
// Cleanup observers
|
||||
private cleanupObservers(): void {
|
||||
this.observers.forEach(observer => observer.disconnect());
|
||||
this.observers = [];
|
||||
}
|
||||
|
||||
// Clear intervals
|
||||
private clearIntervals(): void {
|
||||
this.intervalIds.forEach(id => clearInterval(id));
|
||||
this.intervalIds = [];
|
||||
}
|
||||
|
||||
// Get current metrics
|
||||
getMetrics(): PerformanceMetrics[] {
|
||||
return [...this.metrics];
|
||||
}
|
||||
|
||||
// Get performance summary
|
||||
getPerformanceSummary(): any {
|
||||
return this.analyzeMetrics(this.metrics);
|
||||
}
|
||||
|
||||
// Export metrics for analysis
|
||||
exportMetrics(): string {
|
||||
return JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
metrics: this.metrics,
|
||||
summary: this.getPerformanceSummary(),
|
||||
}, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom performance measurement utilities
|
||||
export class CustomPerformanceMeasurer {
|
||||
private static marks = new Map<string, number>();
|
||||
|
||||
// Mark start of operation
|
||||
static mark(name: string): void {
|
||||
if ('performance' in window && 'mark' in performance) {
|
||||
performance.mark(`${name}-start`);
|
||||
}
|
||||
this.marks.set(name, Date.now());
|
||||
}
|
||||
|
||||
// Measure operation duration
|
||||
static measure(name: string): number {
|
||||
const startTime = this.marks.get(name);
|
||||
const endTime = Date.now();
|
||||
|
||||
if (startTime) {
|
||||
const duration = endTime - startTime;
|
||||
|
||||
if ('performance' in window && 'measure' in performance) {
|
||||
try {
|
||||
performance.measure(name, `${name}-start`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to create performance measure for ${name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.marks.delete(name);
|
||||
return duration;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Measure async operation
|
||||
static async measureAsync<T>(name: string, operation: () => Promise<T>): Promise<T> {
|
||||
this.mark(name);
|
||||
try {
|
||||
const result = await operation();
|
||||
this.measure(name);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.measure(name);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Measure function execution
|
||||
static measureFunction<T extends any[], R>(
|
||||
name: string,
|
||||
fn: (...args: T) => R
|
||||
): (...args: T) => R {
|
||||
return (...args: T): R => {
|
||||
this.mark(name);
|
||||
try {
|
||||
const result = fn(...args);
|
||||
this.measure(name);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.measure(name);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Performance optimization recommendations
|
||||
export class PerformanceOptimizer {
|
||||
// Analyze and provide optimization recommendations
|
||||
static analyzeAndRecommend(metrics: PerformanceMetrics[]): string[] {
|
||||
const recommendations: string[] = [];
|
||||
const latest = metrics[metrics.length - 1];
|
||||
|
||||
if (!latest) return recommendations;
|
||||
|
||||
// FCP recommendations
|
||||
if (latest.fcp && latest.fcp > PERFORMANCE_CONFIG.THRESHOLDS.FCP) {
|
||||
recommendations.push(
|
||||
'Consider optimizing critical rendering path: inline critical CSS, reduce render-blocking resources'
|
||||
);
|
||||
}
|
||||
|
||||
// LCP recommendations
|
||||
if (latest.lcp && latest.lcp > PERFORMANCE_CONFIG.THRESHOLDS.LCP) {
|
||||
recommendations.push(
|
||||
'Optimize Largest Contentful Paint: compress images, preload critical resources, improve server response times'
|
||||
);
|
||||
}
|
||||
|
||||
// Memory recommendations
|
||||
if (latest.memoryUsage) {
|
||||
const usagePercent = (latest.memoryUsage.usedJSHeapSize / latest.memoryUsage.totalJSHeapSize) * 100;
|
||||
|
||||
if (usagePercent > 80) {
|
||||
recommendations.push(
|
||||
'High memory usage detected: implement cleanup routines, check for memory leaks, optimize data structures'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Task loading recommendations
|
||||
if (latest.taskLoadTime && latest.taskLoadTime > 1000) {
|
||||
recommendations.push(
|
||||
'Task loading is slow: implement pagination, optimize database queries, add loading states'
|
||||
);
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
// Get optimization priority
|
||||
static getOptimizationPriority(metrics: PerformanceMetrics[]): Array<{metric: string, priority: 'high' | 'medium' | 'low', value: number}> {
|
||||
const latest = metrics[metrics.length - 1];
|
||||
if (!latest) return [];
|
||||
|
||||
const priorities: Array<{metric: string, priority: 'high' | 'medium' | 'low', value: number}> = [];
|
||||
|
||||
// Check each metric against thresholds
|
||||
if (latest.fcp) {
|
||||
const ratio = latest.fcp / PERFORMANCE_CONFIG.THRESHOLDS.FCP;
|
||||
priorities.push({
|
||||
metric: 'First Contentful Paint',
|
||||
priority: ratio > 2 ? 'high' : ratio > 1.5 ? 'medium' : 'low',
|
||||
value: latest.fcp,
|
||||
});
|
||||
}
|
||||
|
||||
if (latest.lcp) {
|
||||
const ratio = latest.lcp / PERFORMANCE_CONFIG.THRESHOLDS.LCP;
|
||||
priorities.push({
|
||||
metric: 'Largest Contentful Paint',
|
||||
priority: ratio > 2 ? 'high' : ratio > 1.5 ? 'medium' : 'low',
|
||||
value: latest.lcp,
|
||||
});
|
||||
}
|
||||
|
||||
if (latest.cls) {
|
||||
const ratio = latest.cls / PERFORMANCE_CONFIG.THRESHOLDS.CLS;
|
||||
priorities.push({
|
||||
metric: 'Cumulative Layout Shift',
|
||||
priority: ratio > 3 ? 'high' : ratio > 2 ? 'medium' : 'low',
|
||||
value: latest.cls,
|
||||
});
|
||||
}
|
||||
|
||||
return priorities.sort((a, b) => {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track if performance monitoring has been initialized
|
||||
let isInitialized = false;
|
||||
|
||||
// Initialize performance monitoring
|
||||
export const initializePerformanceMonitoring = (): void => {
|
||||
// Prevent duplicate initialization
|
||||
if (isInitialized) {
|
||||
console.warn('Performance monitoring already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
const monitor = EnhancedPerformanceMonitor.getInstance();
|
||||
monitor.startMonitoring();
|
||||
|
||||
// Cleanup on page unload
|
||||
const cleanup = () => {
|
||||
monitor.stopMonitoring();
|
||||
isInitialized = false;
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', cleanup);
|
||||
|
||||
// Also cleanup on page visibility change (tab switching)
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Export global performance utilities
|
||||
export const performanceUtils = {
|
||||
monitor: EnhancedPerformanceMonitor.getInstance(),
|
||||
measurer: CustomPerformanceMeasurer,
|
||||
optimizer: PerformanceOptimizer,
|
||||
initialize: initializePerformanceMonitoring,
|
||||
};
|
||||
319
worklenz-frontend/src/utils/redux-optimizations.ts
Normal file
319
worklenz-frontend/src/utils/redux-optimizations.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import { RootState } from '@/app/store';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
|
||||
// Performance-optimized selectors using createSelector for memoization
|
||||
|
||||
// Basic state selectors (these will be cached)
|
||||
const selectTaskManagementState = (state: RootState) => state.taskManagement;
|
||||
const selectTaskReducerState = (state: RootState) => state.taskReducer;
|
||||
const selectThemeState = (state: RootState) => state.themeReducer;
|
||||
const selectTeamMembersState = (state: RootState) => state.teamMembersReducer;
|
||||
const selectTaskStatusState = (state: RootState) => state.taskStatusReducer;
|
||||
const selectPriorityState = (state: RootState) => state.priorityReducer;
|
||||
const selectPhaseState = (state: RootState) => state.phaseReducer;
|
||||
const selectTaskLabelsState = (state: RootState) => state.taskLabelsReducer;
|
||||
|
||||
// Memoized task selectors
|
||||
export const selectOptimizedAllTasks = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => Object.values(taskManagementState.entities || {})
|
||||
);
|
||||
|
||||
export const selectOptimizedTasksById = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.entities || {}
|
||||
);
|
||||
|
||||
export const selectOptimizedTaskGroups = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.groups || []
|
||||
);
|
||||
|
||||
export const selectOptimizedCurrentGrouping = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.grouping || 'status'
|
||||
);
|
||||
|
||||
export const selectOptimizedLoading = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.loading || false
|
||||
);
|
||||
|
||||
export const selectOptimizedError = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.error
|
||||
);
|
||||
|
||||
export const selectOptimizedSearch = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.search || ''
|
||||
);
|
||||
|
||||
export const selectOptimizedArchived = createSelector(
|
||||
[selectTaskManagementState],
|
||||
(taskManagementState) => taskManagementState.archived || false
|
||||
);
|
||||
|
||||
// Theme selectors
|
||||
export const selectOptimizedIsDarkMode = createSelector(
|
||||
[selectThemeState],
|
||||
(themeState) => themeState?.mode === 'dark'
|
||||
);
|
||||
|
||||
export const selectOptimizedThemeMode = createSelector(
|
||||
[selectThemeState],
|
||||
(themeState) => themeState?.mode || 'light'
|
||||
);
|
||||
|
||||
// Team members selectors
|
||||
export const selectOptimizedTeamMembers = createSelector(
|
||||
[selectTeamMembersState],
|
||||
(teamMembersState) => teamMembersState.teamMembers || []
|
||||
);
|
||||
|
||||
export const selectOptimizedTeamMembersById = createSelector(
|
||||
[selectOptimizedTeamMembers],
|
||||
(teamMembers) => {
|
||||
if (!Array.isArray(teamMembers)) return {};
|
||||
const membersById: Record<string, any> = {};
|
||||
teamMembers.forEach((member: any) => {
|
||||
membersById[member.id] = member;
|
||||
});
|
||||
return membersById;
|
||||
}
|
||||
);
|
||||
|
||||
// Task status selectors
|
||||
export const selectOptimizedTaskStatuses = createSelector(
|
||||
[selectTaskStatusState],
|
||||
(taskStatusState) => taskStatusState.status || []
|
||||
);
|
||||
|
||||
export const selectOptimizedTaskStatusCategories = createSelector(
|
||||
[selectTaskStatusState],
|
||||
(taskStatusState) => taskStatusState.statusCategories || []
|
||||
);
|
||||
|
||||
// Priority selectors
|
||||
export const selectOptimizedPriorities = createSelector(
|
||||
[selectPriorityState],
|
||||
(priorityState) => priorityState.priorities || []
|
||||
);
|
||||
|
||||
// Phase selectors
|
||||
export const selectOptimizedPhases = createSelector(
|
||||
[selectPhaseState],
|
||||
(phaseState) => phaseState.phaseList || []
|
||||
);
|
||||
|
||||
// Labels selectors
|
||||
export const selectOptimizedLabels = createSelector(
|
||||
[selectTaskLabelsState],
|
||||
(labelsState) => labelsState.labels || []
|
||||
);
|
||||
|
||||
// Complex computed selectors
|
||||
export const selectOptimizedTasksByGroup = createSelector(
|
||||
[selectOptimizedAllTasks, selectOptimizedTaskGroups],
|
||||
(tasks, groups) => {
|
||||
const tasksByGroup: Record<string, Task[]> = {};
|
||||
|
||||
groups.forEach((group: any) => {
|
||||
tasksByGroup[group.id] = group.tasks || [];
|
||||
});
|
||||
|
||||
return tasksByGroup;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectOptimizedTaskCounts = createSelector(
|
||||
[selectOptimizedTasksByGroup],
|
||||
(tasksByGroup) => {
|
||||
const counts: Record<string, number> = {};
|
||||
Object.keys(tasksByGroup).forEach(groupId => {
|
||||
counts[groupId] = tasksByGroup[groupId].length;
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectOptimizedTotalTaskCount = createSelector(
|
||||
[selectOptimizedAllTasks],
|
||||
(tasks) => tasks.length
|
||||
);
|
||||
|
||||
// Selection state selectors
|
||||
export const selectOptimizedSelectedTaskIds = createSelector(
|
||||
[(state: RootState) => state.taskManagementSelection?.selectedTaskIds],
|
||||
(selectedTaskIds) => selectedTaskIds || []
|
||||
);
|
||||
|
||||
export const selectOptimizedSelectedTasksCount = createSelector(
|
||||
[selectOptimizedSelectedTaskIds],
|
||||
(selectedTaskIds) => selectedTaskIds.length
|
||||
);
|
||||
|
||||
export const selectOptimizedSelectedTasks = createSelector(
|
||||
[selectOptimizedAllTasks, selectOptimizedSelectedTaskIds],
|
||||
(tasks, selectedTaskIds) => {
|
||||
return tasks.filter((task: Task) => selectedTaskIds.includes(task.id));
|
||||
}
|
||||
);
|
||||
|
||||
// Performance utilities
|
||||
export const createShallowEqualSelector = <T>(
|
||||
selector: (state: RootState) => T
|
||||
) => {
|
||||
let lastResult: T;
|
||||
let lastArgs: any;
|
||||
|
||||
return (state: RootState): T => {
|
||||
const newArgs = selector(state);
|
||||
|
||||
if (!shallowEqual(newArgs, lastArgs)) {
|
||||
lastArgs = newArgs;
|
||||
lastResult = newArgs;
|
||||
}
|
||||
|
||||
return lastResult;
|
||||
};
|
||||
};
|
||||
|
||||
// Memoized equality functions for React.memo
|
||||
export const taskPropsAreEqual = (
|
||||
prevProps: any,
|
||||
nextProps: any
|
||||
): boolean => {
|
||||
// Quick reference checks first
|
||||
if (prevProps.task === nextProps.task) return true;
|
||||
if (!prevProps.task || !nextProps.task) return false;
|
||||
if (prevProps.task.id !== nextProps.task.id) return false;
|
||||
|
||||
// Check other props
|
||||
if (prevProps.isSelected !== nextProps.isSelected) return false;
|
||||
if (prevProps.isDragOverlay !== nextProps.isDragOverlay) return false;
|
||||
if (prevProps.groupId !== nextProps.groupId) return false;
|
||||
if (prevProps.currentGrouping !== nextProps.currentGrouping) return false;
|
||||
if (prevProps.level !== nextProps.level) return false;
|
||||
|
||||
// Deep comparison for task properties that commonly change
|
||||
const taskProps = [
|
||||
'title',
|
||||
'progress',
|
||||
'status',
|
||||
'priority',
|
||||
'description',
|
||||
'startDate',
|
||||
'dueDate',
|
||||
'updatedAt',
|
||||
'sub_tasks_count',
|
||||
'show_sub_tasks'
|
||||
];
|
||||
|
||||
for (const prop of taskProps) {
|
||||
if (prevProps.task[prop] !== nextProps.task[prop]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare arrays with shallow equality
|
||||
if (!shallowEqual(prevProps.task.assignees, nextProps.task.assignees)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shallowEqual(prevProps.task.labels, nextProps.task.labels)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const taskGroupPropsAreEqual = (
|
||||
prevProps: any,
|
||||
nextProps: any
|
||||
): boolean => {
|
||||
// Quick reference checks
|
||||
if (prevProps.group === nextProps.group) return true;
|
||||
if (!prevProps.group || !nextProps.group) return false;
|
||||
if (prevProps.group.id !== nextProps.group.id) return false;
|
||||
|
||||
// Check task lists
|
||||
if (!shallowEqual(prevProps.group.taskIds, nextProps.group.taskIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check other props
|
||||
if (prevProps.projectId !== nextProps.projectId) return false;
|
||||
if (prevProps.currentGrouping !== nextProps.currentGrouping) return false;
|
||||
if (!shallowEqual(prevProps.selectedTaskIds, nextProps.selectedTaskIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Performance monitoring utilities
|
||||
export const createPerformanceSelector = <T>(
|
||||
selector: (state: RootState) => T,
|
||||
name: string
|
||||
) => {
|
||||
return createSelector(
|
||||
[selector],
|
||||
(result) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const startTime = performance.now();
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
if (duration > 5) {
|
||||
console.warn(`Slow selector ${name}: ${duration.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Utility to create batched state updates
|
||||
export const createBatchedStateUpdate = <T>(
|
||||
updateFn: (updates: T[]) => void,
|
||||
delay: number = 16 // One frame
|
||||
) => {
|
||||
let pending: T[] = [];
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
return (update: T) => {
|
||||
pending.push(update);
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
const updates = [...pending];
|
||||
pending = [];
|
||||
timeoutId = null;
|
||||
updateFn(updates);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
// Performance monitoring hook
|
||||
export const useReduxPerformanceMonitor = () => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const startTime = performance.now();
|
||||
|
||||
return () => {
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
if (duration > 16) {
|
||||
console.warn(`Slow Redux operation: ${duration.toFixed(2)}ms`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return () => {}; // No-op in production
|
||||
};
|
||||
Reference in New Issue
Block a user