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:
chamiakJ
2025-07-10 20:39:15 +05:30
parent cf686ef8c5
commit 94977f7255
15 changed files with 3572 additions and 133 deletions

View 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);
});
},
};

View 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;
},
};

View 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,
};

View 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
};